출처 : http://woongsanta.tistory.com/25

1. jakarta Project BeanUtils 소개

요즘 자주 사용하는 스트럿츠, 스프링 등의 프레임워크를 보면 BeanUtils를 자주 사용하는 걸 볼 수 있습니다. 
자바 객체의 속성들을 동적으로 파악해서 필요한 처리를 해야 하는 경우가 점차 증가하고 있는것입니다.

기존의 Reflection와 Introspection API를 이용해서 구현할 수 있지만  API를 이해하고 활용하기가 매우 복잡하고 까다롭기도 합니다. 좀 더 쉽고 편하게 
이용할 수 없을까 하는 needs에 의해 만들어 진게 BeanUtils 컴포넌트입니다.

이 컴포넌트를 처음 본다고 하시는 분도 계시겠지만 실제 우리 소스에서 검색해보면 사용 페이지가 제법 나오기도 하며 스프링 프레임워크가 적용되지 않은 페이지에서 쉽고 편하게 사용할 수 있는 모듈이기도 합니다. 
이클립스에서 BeanConverterUtils, BeanUtils로 검색해보세요^^

일단 우리가 현재 이 모듈을 어떻게 사용하고 있는지 보겠습니다. 
검색을 보면 request로 넘기는 무수히 많을 값을 ReqPrdSearch 객체에 값을 자동으로 
넣어 주는 걸 볼 수 있습니다.

검색을 하면 get 방식으로 다음처럼 값이 넘어 갑니다.
fld=&so=0&mfr=&attr_1=&attr_2=&attr_3=&attr_4=&attr_5=&attr_6=&am1=&am2=&am3=&am4=&am5=&am6=&mfm=&cm=&sn=41&pg=20&rq=&price1=-1&price2=-1&searchColl=ALL&tq=mp3&cat0=&cat1=&cat2=&catdepth=-1&searchListType=A

이 값들을 어떻게 처리할까요?
그냥 useBean 쓰면 안되나요? 안됩니다.^^ 
jsp가 아닌 bean에서 처리할려 하니 쓸 수가 없습니다. 
물론 스프링에서 아래처럼하면 됩니다.
bind(request, ReqPrdSearch);

하지만 스프링를 사용하지 않은 경우 어떻게 하나요?
beanUtils를 이용하시면 됩니다.

request를 객체에 담는거 이외에 또 어디에서 사용하고 있을까요?

DB로 부터 가져온 값을 처리할 때 이용하고 있습니다. 
spring jdbc를 이용해서 DB로부터 값을 가져와서 Map(GSData)에 넣고 이용하는데
그 값을 객체에 넣고 사용할 때가 있습니다. 소스를 추적하다 보면 값넣어 주는 
부분이 안보이는데 잘 작동이 되는 걸 볼 수 있습니다.

알게 모르게 이미 우리 안에서 사용되고 있으며 
알면 편하고 쉽게 쓸 수 있어서 간단하게 BeanUtils를 소개합니다.

2. reference 
http://jakarta.apache.org/commons/beanutils/
이곳에 가시면 각종 guide문서와 최신 jar파일을 다운 받으실수 있습니다.

http://jakarta.apache-korea.org/commons/beanutils.html
영어에 거부 반응이 있으신분은 여기 가보시면 번역된 내용을 접하실 수 있습니다.

http://www.jakartaproject.com/
자카르타프로젝트라는 책을 쓴 최범균씨 홈페이지인데 좋은 정보를 얻을 수 있습니다.

3. 주요 클래스
3-1. BeanUtils
     객제의 정보, 속성 값 읽기, 속성 값 쓰기 등의 기능을 제공하고 있으며
     PropertyUtils와 차이점은 value를 convert 해준다는 겁니다. 
3-2. PropertyUtils
     BeanUtils와 기능은 거의 흡사합니다. 
3-3. ConvertUtils
     타입에 따라 convet 하는 기능을 수행합니다. 
     
4. 상세 내용
   BeanUtils를 사용하기 위해서는 몇가지 규칙이 있습니다. 
   관련 부분은 필요시 사이트나 책을 찾아 보시면 나옵니다.

   BeanUtils, PropertyUtils, ConvertUtils는 모두 static한 메소드를 가지고 있으며
   BeanUtilsBean, PropertyUtilsBeanUtilsBean, ConvertUtilsBeanUtilsBean 이라는 
   싱글톤으로 구현된 객체와 wrapping되어 있는걸 볼 수 있습니다. 
   ...
   public static Object cloneBean(Object bean)
           throws IllegalAccessException, InstantiationException,
           InvocationTargetException, NoSuchMethodException {
       return BeanUtilsBean.getInstance().cloneBean(bean);
   }
   ...
   나름대로 참고할 만한 구조 같습니다. ^^ 
   
4-1. BeanUtils

// bean 복제
public static Object cloneBean(Object bean)

// orig에서 dest로 복제, 동일 속성명이 존재해야합니다.
public static void copyProperties(Object dest, Object orig)

// orig의 property를 dest로 복제
public static void copyProperty(Object bean, String name, Object value)

// Return the value of the specified array property of the specified bean, as a String array.
public static String[] getArrayProperty(Object bean, String name)

// 배열값 가져오기, property name을 'name[0]' 이런 식으로 주어야 한다. 규칙임
public static String getIndexedProperty(Object bean, String name)

// 배열값 가져오기 index는 몇번째
public static String getIndexedProperty(Object bean, String name, int index)

// mapped property 가져오기, property name을 'name(0)' 이런 식으로 주어야 한다.
public static String getMappedProperty(Object bean, String name)
// mapped property 가져오기
public static String getMappedProperty(Object bean, String name, String key)

public static String getNestedProperty(Object bean, String name)

// bean에서 값 가져오기
public static String getProperty(Object bean, String name)

public static String getSimpleProperty(Object bean, String name)

// bean에서 해당 name의 property에 value를 convert해서 넣는다.
public static void setProperty(Object bean, String name, Object value)

/*
 * bean 있는 값을 key, value로 map에 넣어 줍니다. 가장 많이 쓰이는 메소드 중 하나
 */
public static Map describe(Object bean)

/*
 * map에 있는 값을 bean에 넣어 줍니다. 가장 많이 쓰이는 메소드 중 하나
 */
public static void populate(Object bean, Map properties)

describe메소드를 이용해서 객체의 property와 value, type등을 쉽게 알아낼 수 있습니다.

Map map = BeanUtils.describe(bean);
Set set = map.keySet();
Iterator it = set.iterator();
while (it.hasNext()) {
    String key = (String)it.next();

    if ("class".equals(key)) {
        continue;
    }
    Object value = map.get(key);
    properties.put(key, value);
}
             
4-2. PropertyUtils 
     BeanUtils와 거의 동일합니다. 
     BeanUtils가 bean를 다루기위한거라면 PropertyUtils는 map을 처리한다고 보시면
     됩니다.

4-3. ConvertUtils
     이 utils은 싱글톤으로 구현된 ConvertUtilsBean에서 맵에 각종 컨버터를 
     등록해 놓고 lookup(Class clazz)해서 converter 얻고 그걸로 값을 처리하고 
     있습니다.
     조금만 수정하면 아주 잘 써먹을 수 있는 util이길래 언급합니다.

5. example & test

배열, 기본형, 객체를 property로 하는 dto들을 만들어 잘 처리가 되는지 확인합니다.

Person.java : dto안에 또 다른 dto를 넣어서 잘 처리되는지 확인하기 위해 만들었습니다.

public class Person {
    private String name;
    private int age;

    public Person(String name, int age)
    {
        this.name = name;
        this.age = age;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }

    public String toString()
    {
        final String TAB = "    ";

        String retValue = "";

        retValue = "Person ( "
            + "name = " + this.name + TAB
            + "age = " + this.age + TAB
            + " )";

        return retValue;
    }
}


Employee.java : 기본형, 배열, 객체 등을 property로 생성합니다.

public class Employee {
    private String address;
    private String firstName;
    private String lastName;
    private int age;
    private Person person;
    private Date credate;
    private List personList;
    private String[] fld1;
    private int[] fld2;

    public String[] getFld1() {
        return fld1;
    }
    public void setFld1(String[] fld1) {
        this.fld1 = fld1;
    }
    public int[] getFld2() {
        return fld2;
    }
    public void setFld2(int[] fld2) {
        this.fld2 = fld2;
    }
    public List getPersonList() {
        return personList;
    }
    public void setPersonList(List personList) {
        this.personList = personList;
    }
    public String getAddress() {
        return address;
    }
    public void setAddress(String address) {
        this.address = address;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }
    public Date getCredate() {
        return credate;
    }
    public void setCredate(Date credate) {
        this.credate = credate;
    }
    public String getFirstName() {
        return firstName;
    }
    public void setFirstName(String firstName) {
        this.firstName = firstName;
    }
    public String getLastName() {
        return lastName;
    }
    public void setLastName(String lastName) {
        this.lastName = lastName;
    }
    public Person getPerson() {
        return person;
    }
    public void setPerson(Person person) {
        this.person = person;
    }
    public String toString()
    {
        final String TAB = "    ";

        String retValue = "";

        retValue = "Employee ( "
            + "address = " + this.address + TAB
            + "firstName = " + this.firstName + TAB
            + "lastName = " + this.lastName + TAB
            + "age = " + this.age + TAB
            + "person = " + this.person + TAB
            + "credate = " + this.credate + TAB
            + "personList = " + this.personList + TAB
            + " )";

        return retValue;
    }

}


BeanConvertUtils.java

public class BeanConvertUtils {
    //map의 value을 bean에 넣어주는 메소드
 public static void mapToBean(java.util.Map properties, java.lang.Object bean)
   throws IllegalAccessException, InvocationTargetException,
   NoSuchMethodException {

  if (properties == null) {
   return;
  }

  BeanUtils.populate(bean, properties);

 }

    //bean의 value을 map에 넣어주는 메소드
 public static void beanToMap(java.lang.Object bean, java.util.Map properties)
   throws IllegalAccessException, InvocationTargetException,
   NoSuchMethodException {

  Map map = PropertyUtils.describe(bean);

  map.remove("class");
  properties.putAll(map);

 }
}


TestBeanConvertUtils : 테스트 케이스

public class TestBeanConvertUtils extends TestCase {
    Employee emp;
    GSData map;
    HttpServletRequestMock request;
    public void setUp() {
        List list = new ArrayList();
        list.add(new Person("kkaok1", 23));
        list.add(new Person("kkaok2", 22));

        emp = new Employee();
        emp.setAddress("경기도");
        emp.setFirstName("kim");
        emp.setLastName("hyun");
        emp.setPerson(new Person("kkaok", 22));
        emp.setPersonList(list);
        emp.setAge(22);
        emp.setFld1(new String[]{"0","1","2"});

        map = new GSData();
        //map = new HashMap();
        map.put("address", "경기도");
        map.put("firstName", "kim");
        map.put("lastName", "hyun");
        map.put("person", new Person("kkaok", 22));
        map.put("personList", list);
        map.put("age", new Integer(22));
        map.put("fld1", new String[]{"0","1","2"});
    }

    public void testmapToBean() throws IllegalAccessException, InvocationTargetException, NoSuchMethodException{
        Integer i1= new Integer(0);

        new ConvertUtilsBean().convert("0", i1.getClass());

        Employee emptest = new Employee();
        BeanConvertUtils.mapToBean(map, emptest);
        //System.out.println(emp.toString());
        //System.out.println(emptest.toString());
        assertEquals(emp.toString(), emptest.toString());
    }

    public void testbeanToMap() throws IllegalAccessException, InvocationTargetException, NoSuchMethodException{
        Map maptest = new HashMap();
        BeanConvertUtils.beanToMap(emp, maptest);
        //System.out.println(map.toString());
        //System.out.println("maptest : "+maptest.toString());

        assertEquals(map.get("address"), maptest.get("address"));
        assertEquals(map.get("age"), maptest.get("age"));
    }

}



6. 마무리

   map에 있는 값을 bean에 넣거나
   bean에 있는 값을 map에 넣거나
   request에 있는 값을 bean에 넣거나
   동적으로 변하는 bean을 분석해야 할때 정말 편하고 쉽게 쓸 수 있는 모듈입니다. 

   한번 소스를 뜯어 보시면 좋은 내용을 접하실 기회가 되실 거라 믿습니다.

7. 참고

   eclipse에서 테스트 하실때는 commons-beanutils.jar, commons-beanutils-bean-collections.jar,  commons-beanutils-core.jar, commons-logging-1.1.jar, junit-3.8.1.jar 등을 빌드패스에 넣어 주세요 

  * request의 값을 빈에 넣는 예)
  BeanUtils.populate(객체, request.getParameterMap());
  * request.getParameterMap() 이렇게 하면 map에 key, value로 값이 return 됩니다. 


Thymeleaf : http://www.thymeleaf.org
Thymeleaf Spring mail 샘플 : http://www.thymeleaf.org/springmail.html
Thymeleaf 태그 설명 : http://www.thymeleaf.org/standarddialect5minutes.html
git : https://github.com/thymeleaf
Thymeleaf Tiles 결합 : https://github.com/thymeleaf/thymeleaf-extras-tiles2


[참고]
james 메일 보내기 : http://tyboss.tistory.com/entry/Java-James-서버-사용하여-메일mail-보내기


Inputstream 사용 : https://stackoverflow.com/questions/53323313/java-thymeleaf-how-to-process-an-inputstream-in-templateengine-stand-alon


1. Maven으로 Thymeleaf Library 추가하기

 <dependency>
      <groupId>org.thymeleaf</groupId>
      <artifactId>thymeleaf</artifactId>
      <version>2.0.16</version>
 </dependency>
 <dependency>
      <groupId>org.thymeleaf</groupId>
      <artifactId>thymeleaf-spring3</artifactId>
      <version>2.0.16</version>
 </dependency>




2. Spring에 Thymeleaf 설정하기

 <bean id="emailTemplateResolver" class="org.thymeleaf.templateresolver.ClassLoaderTemplateResolver">
   <!--
    ClassLoaderTemplateResolver의 기본 경로는 소스 경로 즉 /WEB-INF/classes 부터 시작이다.
    -->
   <property name="prefix" value="templates/" />
   <property name="suffix" value=".html" />
   <property name="templateMode" value="HTML5" />
   <property name="characterEncoding" value="UTF-8" />
   <!-- <property name="cacheable" value="false" /> -->
   <property name="order" value="1" />
  </bean>
  
   <!--
   ServletContextTemplateResolver는 sitemesh나 tiles와 경로 설정은 똑같이 하면 된다.
   Jrebel 적용시 ClassLoaderTemplateResolver는 cacheable를 false로 해도 적용이 되지 않기 때문에 ServletTemplateResolver로 테스트 후 바꿔도 된다.

   -->
   <bean id="webTemplateResolver" class="org.thymeleaf.templateresolver.ServletContextTemplateResolver">
   <property name="prefix" value="/WEB-INF/presentation/jsp/web/templates/" />
   <property name="suffix" value=".html" />
   <property name="templateMode" value="HTML5" />
   <property name="characterEncoding" value="UTF-8" />
   <!-- <property name="cacheable" value="false" /> -->
   <property name="order" value="2" />
  </bean>  
  
  
  <bean id="templateEngine" class="org.thymeleaf.spring3.SpringTemplateEngine">
    <property name="templateResolvers">
      <set>
         <ref bean="emailTemplateResolver" />
         <ref bean="webTemplateResolver" />
      </set>
    </property>
    <!-- Resolver를 한개만 사용할 경우
    <property name="templateResolver" ref="emailTemplateResolver" />
    -->
  </bean>



3. Template 만들기
    다음에서 설명하겠지만 Context.setVariable(key, value) 로 변수값을 설정 후에 아래 빨간색 부분처럼 사용할 수 있다. 일반 텍스트는 th:text="${key}" 로 사용하고 URL은 th:href="@{${key}}" 로 사용한다.

- template.html

 <!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
    <head>
        <title th:remove="all">Template for HTML email with inline image</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
    </head>
    <body>
        <span th:text="${name}"></span>
        <p th:text="#{greeting(${name})}">
            Hello, Peter Static!
        </p>
        <p th:if="${name.length() > 10}">
            Wow! You've got a long name (more than 10 chars)!
        </p>
        <p>
            You have been successfully subscribed to the <b>Fake newsletter</b> on
            <span th:text="${#dates.format(subscriptionDate)}">28-12-2012</span>
        </p>
        <p>Your hobbies are:</p>
        <ul th:remove="all-but-first">
            <li th:each="hobby : ${hobbies}" th:text="${hobby}">Reading</li>
            <li>Writing</li>
            <li>Bowling</li>
        </ul>
        <p>
            You can find <b>your inlined image</b> just below this text.
        </p>
        <p>
            <img src="sample.png" th:src="'cid:' + ${imageResourceName}" />
        </p>
        <p>
                 <a th:href="@{${homepageUrl}}" style="text-decoration:none;">내 홈페이지</a><br />
                 <a th:href="@{'http://' + ${naverUrl}}" style="text-decoration:none;">네이버</a>
        </p>
        <p>
            Regards, <br />
            &emsp; <em>The Thymeleaf Team</em>
        </p>
    </body>
</html>



4. Controller에서 Template 불러오기

    4-1. ClassLoaderTemplateResolver 사용시

@RequestMapping("mail.do")
 public void mail(@RequestParam Map<String, Object> paramMap,
   @Value("#{global['server.host']}") String serverHost,
   HttpServletRequest request,
   final Locale locale,
   ModelMap model) throws Exception {
  log.debug("***************************************************************************************");
  log.debug("*** paramMap : " + paramMap);
  log.debug("***************************************************************************************");
  
  final Context ctx = new Context(locale);

// 변수 설정
ctx.setVariable("name", "홍길동");

/*
  Map<String, String> mailMap = new HashMap<String, String>();
  String usrKey = RandomStringUtils.random(20, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
  mailMap.put("usrKey", usrKey);
  mailMap.put("serverUrl", "http://" + serverHost + request.getContextPath());
  mailMap.put("usrNm", "홍길동");
  mailMap.put("loginId", "honggd");

  if(paramMap != null) {
   Iterator<String> iter = paramMap.keySet().iterator();
   String key = null;
   while(iter.hasNext()) {
    key = iter.next();
    ctx.setVariable(key, paramMap.get(key));
   }
  }
*/

  String template = templateEngine.process("template", ctx);  
  log.debug(template);  // html template가 변수로 치환되어 String으로 반환한다. 이 String을 메일 보낼 때 내용으로 사용하면 된다.
 }


    4-2. ServletContextTemplateResolver 사용시

@Autowired
private ServletContext servletContext;

@RequestMapping(value="/send.do", method=RequestMethod.POST)
 public void send(@RequestParam Map<String, Object> paramMap,
   @Value("#{global['server.host']}") String serverHost,
   @Value("#{global['mail.admin.email']}") String mailAdminEmail,
   @Value("#{global['mail.admin.name']}") String mailAdminName,
   HttpServletRequest request,
   HttpServletResponse response,
   final Locale locale,
   ModelMap model) throws Exception{
  log.debug("***************************************************************************************");
  log.debug("*** paramMap : " + paramMap);
  log.debug("***************************************************************************************");
  
  model.addAttribute("paramMap", paramMap);
  
  // ClassLoaderTemplateResolver 를 사용할 때와 Context를 생성하는 부분만 다르다.
  final WebContext ctx = new WebContext(request, response, servletContext, locale);
 
  ctx.setVariable("usrNm", "홍길동");  

  String template = templateEngine.process("template", ctx); 
  log.debug(template);
}


  4-3. StringTemplateResolver 사용시

StringTemplateResolver templateResolver = new StringTemplateResolver();
templateResolver.setTemplateMode(TemplateMode.HTML);
templateResolver.setCacheable(false);

TemplateEngine templateEngine = new TemplateEngine();
templateEngine.setTemplateResolver(templateResolver);
IContext context = new Context(SpringUtil.getLocale(), dataBox);
String out = templateEngine.process(html, context);


 


http://www.opensymphony.com/sitemesh/


1. 3.0 버전

Homepage : http://www.sitemesh.org/

Download : http://github.com/sitemesh/sitemesh3/downloads

Document : http://www.sitemesh.org/configuration.html





2. 2.4.2 버전 이하

Download : https://java.net/downloads/sitemesh/

Document : http://pratinas.net/wiki/SiteMesh


/WEB-INF/web.xml

 <filter>
        <filter-name>sitemesh</filter-name>
        <filter-class>com.opensymphony.sitemesh.webapp.SiteMeshFilter</filter-class>
    </filter>

   <filter-mapping>
  <filter-name>sitemesh</filter-name>
        <url-pattern>*.do</url-pattern>
        <dispatcher>REQUEST</dispatcher>
        <dispatcher>FORWARD</dispatcher>
        <dispatcher>ERROR</dispatcher>
 </filter-mapping>



/WEB-INF/sitemesh.xml

 <?xml version="1.0" encoding="UTF-8" ?>
<sitemesh>
    <property name="decorators-file" value="/WEB-INF/config/decorators.xml"/>
    <excludes file="${decorators-file}"/>
 
    <page-parsers>
     <parser content-type="text/html" class="com.opensymphony.module.sitemesh.parser.HTMLPageParser" />
    </page-parsers>
   
    <decorator-mappers>
        <mapper class="com.opensymphony.module.sitemesh.mapper.PageDecoratorMapper">
   <!--
   decorator 가 적용되는 순서이다. 에러처리시 유용하다.
   Spring이든 Struts 등 500, 404 jsp 에러페이지에 <meta name="decorator" content="error" />
   를 추가한 후 아래의 decorators.xml에 decorator에 name을 meta content와 같은 error와 지정해주면
   url pattern보다 먼저 적용된다.

   -->
   <param name="property.1" value="meta.decorator" />
   <param name="property.2" value="decorator" />
  </mapper>

  <mapper class="com.opensymphony.module.sitemesh.mapper.FrameSetDecoratorMapper">
  </mapper>

  <mapper class="com.opensymphony.module.sitemesh.mapper.AgentDecoratorMapper">
   <param name="match.MSIE" value="ie" />
   <param name="match.Mozilla [" value="ns" />
   <param name="match.Opera" value="opera" />
   <param name="match.Lynx" value="lynx" />
  </mapper>

  <mapper class="com.opensymphony.module.sitemesh.mapper.PrintableDecoratorMapper">
   <param name="decorator" value="printable" />
   <param name="parameter.name" value="printable" />
   <param name="parameter.value" value="true" />
  </mapper>

  <mapper class="com.opensymphony.module.sitemesh.mapper.RobotDecoratorMapper">
   <param name="decorator" value="robot" />
  </mapper>

  <mapper class="com.opensymphony.module.sitemesh.mapper.ParameterDecoratorMapper">
   <param name="decorator.parameter" value="decorator" />
   <param name="parameter.name" value="confirm" />
   <param name="parameter.value" value="true" />
  </mapper>

  <mapper class="com.opensymphony.module.sitemesh.mapper.FileDecoratorMapper">
  </mapper>

  <mapper class="com.opensymphony.module.sitemesh.mapper.ConfigDecoratorMapper">
   <param name="config" value="${decorators-file}" />
  </mapper>
    </decorator-mappers>
   
</sitemesh>



/WEB-INF/config/decorators.xml

 <?xml version="1.0" encoding="UTF-8" ?>
<decorators>

    <excludes>
        <pattern>*.jsp</pattern>
    </excludes>
 
 
    <decorator name="none">
        <pattern>/common/*</pattern>
    </decorator>
   
    <decorator name="simple" page="/WEB-INF/views/common/decorators/simple.jsp">
        <pattern>/prototype/*Pop.do</pattern>
        <pattern>/prototype/*Simple.do</pattern>
    </decorator>
 
    <decorator name="default" page="/WEB-INF/views/common/decorators/default.jsp">
        <pattern>/prototype/*.do</pattern>
    </decorator>
   
    <decorator name="error" page="/WEB-INF/views/common/decorators/error.jsp" />

</decorators>



/WEB-INF/views/common/decorators/default.jsp
 <?xml version="1.0" encoding="UTF-8" ?>
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib uri="http://www.opensymphony.com/sitemesh/decorator" prefix="decorator" %>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.1//EN" "http://www.w3.org/TR/xhtml11/DTD/xhtml11.dtd">
<html version="-//W3C//DTD XHTML 1.1//EN" xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.w3.org/1999/xhtml http://www.w3.org/MarkUp/SCHEMA/xhtml11.xsd">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" />
<title>[:: <decorator:title default="프로젝트" /> ::]</title>
<decorator:head />
</head>
<body>
<h1>Header</h1>
<decorator:body />
<h2>Footer</h2>
</body>
</html>









+ Recent posts