Whiteship's Note

'전체'에 해당되는 글 2638건

  1. 2010.06.14 [회사일] mgt.jsp 파일에 태그 파일 적용
  2. 2010.06.11 [회사일] edit.jsp 화면에 태그 파일 적용하기
  3. 2010.06.11 [회사일] GenericController 만들기
  4. 2010.06.11 [회사일] Generic 타입 추론 유틸 만들기
  5. 2010.06.10 [Hot potatoes] 문제 만들기 프로그램 (4)
  6. 2010.06.10 [집안일] 바느질하는 기선이 (4)
  7. 2010.06.09 [회사일] GenericService 버그 수정하기 (3)
  8. 2010.06.09 [회사일] MemberService 만들기. GenericService 만들기 (6)
  9. 2010.06.09 [회사일] Member 추가. MemberDAO 구현. GenericDAO 구현
  10. 2010.06.09 [회사일] CriteriaUtils 테스트하기 (2)
  11. 2010.06.09 [회사일] 검색에 enum 필드 추가하기
  12. 2010.06.09 [JSP 에디터] 인텔리J는 정말 똑똑해.
  13. 2010.06.08 [회사일] Enum 추가, Formatter 적용
  14. 2010.06.08 [회사일] GenericPersistentEnumFormatter 만들기
  15. 2010.06.08 [회사일] CRUD 화면 디자인 수정
  16. 2010.06.07 봄싹 마니산 등정 성공 (4)
  17. 2010.06.07 [회사일] CRUD 구현
  18. 2010.06.07 [회사일] add 팝업 만들기
  19. 2010.06.04 [스프링 퀴즈] @Autowired (4)
  20. 2010.06.03 [회사일] 그리드 정렬 기능 구현하기
  21. 2010.06.03 [회사일] 검색및 페이지 처리 구현하기
  22. 2010.06.03 [회사일] 그리드 출력하기 with JqGrid
  23. 2010.06.03 [회사일] 화면 레이아웃 잡기
  24. 2010.06.02 [헤드 퍼스트 아이폰] 실습 완료!!
  25. 2010.06.01 [회사일] DAO 테스트 만들기
  26. 2010.06.01 [회사일] 초간단 계층형 아키텍처 만들기
  27. 2010.06.01 [회사일] 프로젝트 세팅 (2)
  28. 2010.06.01 [회사일] 간단한 재고 조사 시스템 개발
  29. 2010.05.31 [헤드 퍼스트 아이폰] 막힌 곳 돌파! (2)
  30. 2010.05.30 [헤드 퍼스트 아이폰] 드디어 막혔다;;

[회사일] mgt.jsp 파일에 태그 파일 적용

프로젝트/SLT : 2010.06.14 11:38



가운데는 그리드가 와야하고 오른쪽에는 검색 화면이 나와야하는 페이지. 이 페이지에 태그 파일을 적용해서...

<page:mgtpage label="계정 관리">
    <page:search label="계정 검색">
        <s:input label="이름" path="name" />
        <s:input label="이메일" path="email" />
    </page:search>
    <script type="text/javascript">
        $(function() {
            $("#smdis-grid").jqGrid({
                colNames:['id', '아이디', '이름', '이메일'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'loginId', index:'loginId', width:80},
                    {name:'name', index:'name', width:90},
                    {name:'email', index:'email', width:90},
                ]
            });
        });
    </script>
</page:mgtpage>

이런식으로 만들었습니다. 딱 봐도.. 검색 부분에 머머가 있구나.. 그리드에는 머머가 있구나가 보이니깐 이정도면 만족합니다. 태그 파일을 계속 써보니까 그 장점 중 하나가 태그에 좀 더 의미있는 이름을 줄 수 있다는 것이 있더라구요. 대신 단점으로는 이 태그 파일만 가지고 테스트 할 수 없다는게 좀 안타깝습니다. 계속해서 서버를 띄워둔 상태에서 릴로딩 해가면서 확인하고 있는데.. 아마 프리마커 같은 템플릿 엔진의 장점은 바로 테스트 용이성이 아닐까 싶습니다.

머 어쨋든 지금은 일단 JSP + 태그파일로 후딱 만들기로 했습니다. 
나중에 프리마커 + 매크로를 사용해서 대체해보는 방법도 생각해볼 수 있겠지만.. 머 나중에;

태그 파일을 분류해서 page는 페이지 구조와 관련된 태그를 만들고..
s는 검색에 사용할 폼 태그들을 만들고..
f는 수정이나 추가할 때 사용할 폼 태그들을 만들기로 했습니다.
 
top


[회사일] edit.jsp 화면에 태그 파일 적용하기

프로젝트/SLT : 2010.06.11 14:05


먼저 Code 도메인의 edit.jsp 코드를 보겠습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<page:page>
    <page:head/>
    <page:body>
        <div class="ui-layout-center">
            <h2 class="smdis-pane-title" id="smdis-grid-title">
                <ul class="smdis-left-icons">
                    <li id="smdis-save-button" class="ui-state-default ui-corner-all" title="저장"><span class="ui-icon ui-icon-disk"></span></li>
                    <li id="smdis-delete-button" class="ui-state-default ui-corner-all" title="삭제"><span class="ui-icon ui-icon-trash"></span></li>
                    <li id="smdis-reload-button" class="ui-state-default ui-corner-all" title="원래값으로"><span class="ui-icon ui-icon-refresh"></span></li>
                </ul>
                <ul class="smdis-right-icons">
                    <li id="smdis-list-button" class="ui-state-default ui-corner-all" title="목록으로"><span class="ui-icon ui-icon-search"></span></li>
                    <li id="smdis-back-button" class="ui-state-default ui-corner-all" title="뒤로"><span class="ui-icon ui-icon-arrowreturnthick-1-w"></span></li>
                </ul>
                코드 수정
            </h2>
            <form:form commandName="model" action="/base/code/${model.id}" method="PUT">
                <p class="ui-widget-content"><label>코드종류</label><form:select path="cate" items="${ref.codeCateList}" itemValue="value" itemLabel="descr" /><form:errors path="cate" cssClass="smdis-error-message"/></p>
                <p class="ui-widget-content"><label>코드값</label><form:input path="code"/><form:errors path="code" cssClass="smdis-error-message"/></p>
                <p class="ui-widget-content"><label>코드명</label><form:input path="name"/><form:errors path="name" cssClass="smdis-error-message"/></p>
                <p class="ui-widget-content"><label>설명</label><form:textarea path="descr"/></p>
            </form:form>
        </div>

        <%--//검색/정렬--%>
        <div class="ui-layout-east">
            <h2 class="smdis-pane-title">
                <ul class="smdis-left-icons">
                    <li id="smdis-grid-apply" class="ui-state-default ui-corner-all" title="적용"><span class="ui-icon ui-icon-arrowthickstop-1-w"></span></li>
                </ul>
                <ul class="smdis-right-icons">
                    <li class="ui-state-default ui-corner-all" title="적용하고 닫기"><span class="ui-icon ui-icon-arrowthickstop-1-e"></span></li>
                </ul>
                검색
            </h2>
        </div>

    <script type="text/javascript">
        $(function() {
            $("button").button();

            $("#smdis-save-button").click(function(){
                var result = confirm("저장 하시겠습니까?");
                if(!result) return;

                $("form input[name='_method']").attr("value", "PUT");
                $("form").submit();
            });

            $("#smdis-delete-button").click(function(){
                var result = confirm("삭제 하시겠습니까?");
                if(!result) return;
                
                $("form input[name='_method']").attr("value", "DELETE");
                $("form").submit();
            });

            $("#smdis-reload-button").click(function(){
                var result = confirm("변경 사항을 취소 하시겠습니까?");
                if(!result) return;

                $(document).attr("location", '/base/code/${model.id}/form');
            });

            $("#smdis-back-button").click(function(){
                $(document).attr("location", '/base/code/${model.id}');
            });

            $("#smdis-list-button").click(function(){
                $(document).attr("location", '/base/code/mgt');    
            });
        });
    </script>
    </page:body>
</page:page>

일부는 page라는 태그파일을 적용해서 코드를 많이 줄여놨습니다. 그래도 너무 길고.. Member의 edit.jsp 파일을 만들 때 손대는 곳은 굵은 글씨 밖에 없습니다. 신경쓸 곳만 신경쓰게 만들고 나머진 신경쓰고 싶지도 않고 보고 싶지도 않습니다. 그리고 위와 같은 코드가 여러 JSP에 중복되면.. 결국 자바 중복 코드랑 다를께 없습니다. 화면 코드에서도 중복을 제거해야 합니다.

Member 도메인의 edit.jsp 파일은 결국 다음과 같이 바꼈습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="osaf" tagdir="/WEB-INF/tags/osaf" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<page:page>
    <page:head/>
    <page:body>
        <page:edit label="계정 수정">
            <osaf:input label="로그인ID" path="loginId"/>
            <osaf:input label="비밀번호" path="password"/>
            <osaf:input label="이름" path="name"/>
            <osaf:input label="이메일" path="email"/>
        </page:edit>
        <page:searchWithoutButtons />
    </page:body>
</page:page>

정말 이게 전부입니다. 잡습 코드도 잡습이 적용될 코드가 들어있는 태그 파일로 넣어놨습니다. Code의 edit.jsp 파일에도 태그 파일을 적용하면..

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="osaf" tagdir="/WEB-INF/tags/osaf" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<page:page>
    <page:head/>
    <page:body>
        <page:edit label="코드 수정">
            <osaf:select label="코드종류" path="cate" items="${ref.codeCateList}" itemValue="value" itemLabel="descr" />
            <osaf:input label="코드값" path="code"/>
            <osaf:input label="코드명" path="name"/>
            <osaf:textarea label="설명" path="descr"/>
        </page:edit>
        <page:searchWithoutButtons />
    </page:body>
</page:page>

이렇게 됩니다. 사실 저기서 위아래 세줄 정도도 관심 대상이 아니라서 page:editpage 같은 태그 파일을 만들어 줄일 수도 있습니다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="osaf" tagdir="/WEB-INF/tags/osaf" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>

<page:editpage label="코드 수정">
    <osaf:select label="코드종류" path="cate" items="${ref.codeCateList}" itemValue="value" itemLabel="descr" />
    <osaf:input label="코드값" path="code"/>
    <osaf:input label="코드명" path="name"/>
    <osaf:textarea label="설명" path="descr"/>
</page:editpage>

좋아 좋아!!! 이제부터는 이렇게 딱 다섯줄만 코딩하면...


이런 화면을 만들 수 있는 겁니다.

이대로 끝내면 아쉬우니까 태그파일 코드 몇개만 공개하겠습니다.

WEB-INF/tags/page/editpage.tag

<%@ tag pageEncoding="utf-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ attribute name="label" required="true"%>

<page:page>
    <page:head/>
    <page:body>
        <page:edit label="${label}">
            <jsp:doBody/>
        </page:edit>
        <page:searchWithoutButtons />
    </page:body>
</page:page>

WEB-INF/tags/osaf/select.tag

<%@ tag pageEncoding="utf-8" %>
<%@ taglib prefix="page" tagdir="/WEB-INF/tags/page" %>
<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %>
<%@ attribute name="label" required="true"%>
<%@ attribute name="path" required="true"%>
<%@ attribute name="items" type="java.util.List" required="true"%>
<%@ attribute name="itemValue" required="false"%>
<%@ attribute name="itemLabel" required="false"%>

<p class="ui-widget-content">
    <label>${label}</label>
    <form:select path="${path}" items="${items}" itemValue="${itemValue}" itemLabel="${itemLabel}" />
    <form:errors path="${path}" cssClass="smdis-error-message"/>
</p>
top


[회사일] GenericController 만들기

프로젝트/SLT : 2010.06.11 12:46


CodeController에서 구체적인 정보는 최대한 추상화 시킵니다. 예를 들어 "code" 같은 문자열은 별로 좋치 않습니다. 이걸 차라리 "model"이라는 추상적인 이름으로 바꾸면 다른 도메인에서도 사용하기 좋을 겁니다. 애초에 문자열 자체가 그다지 추상화는데 좋치 않긴 하지만 그렇게라도 해야지요. ㅋ

그렇게 해서 일단 추상화 시킨 코드가 이렇습니다.

@Controller
@RequestMapping("/base/code")
@SessionAttributes("model")
public class CodeController {

    @Autowired CodeService codeService;
    @Autowired CodeRef ref;
    String baseUrl;

    public CodeController() {
        RequestMapping rm = this.getClass().getAnnotation(RequestMapping.class);
this.baseUrl = rm.value()[0];
    }

    @ModelAttribute("ref")
public CodeRef ref() {
return ref;
}

    @RequestMapping(value="/mgt")
    public void mgt(Model model){
        model.addAttribute("searchParam", new CodeSearchParam());
    }

    @RequestMapping(method = RequestMethod.GET)
    public void list(Model model, CodeSearchParam searchParam, PageParam pageParam) {
        model.addAttribute("list", codeService.list(pageParam, searchParam));
    }

    @RequestMapping(value = "/form", method = RequestMethod.GET)
    public String addForm(Model model){
        model.addAttribute("model", new Code());
        return baseUrl + "/new";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addFormSubmit(Model model, @Valid @ModelAttribute("model") Code code, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return baseUrl + "/new";
        status.setComplete();
        codeService.add(code);
        return  "redirect:" + baseUrl + "/mgt";
    }

    @RequestMapping(value = "/{id}/form", method = RequestMethod.GET)
    public String editForm(@PathVariable int id, Model model){
        model.addAttribute("model", codeService.getById(id));
        return baseUrl + "/edit";
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    public String editFormSubmit(Model model, @Valid @ModelAttribute("model") Code code, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return baseUrl + "/edit";
        status.setComplete();
        codeService.update(code);
        return  "redirect:" + baseUrl + "/mgt";
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public String delete(@PathVariable int id){
        codeService.deleteBy(id);
        return  "redirect:" + baseUrl + "/mgt";
    }


    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public String view(@PathVariable int id, Model model){
        model.addAttribute("model", codeService.getById(id));
        return baseUrl + "/view";
    }

}

이전 코드에 비해서 달라진게 별로 없지만.. 일단은 baseUrl 속성을 둬서 @RequestMapping에 넣어주는 값을 읽어서 사용하도록 코드를 수정했습니다. 그리고 "code"를 "model"로 바꿨고, "codeList"를 "list"로 바꿨습니다. "ref"애초부터 추상화 시킨 이름을 사용했으니 손댈 필요가 없었습니다.

다음에 할 작업은 화면과 연결하는 겁니다. 화면에 전달하는 객체 이름이 바꼈으니 화면에서 EL 부분을 손봐줍니다.

다음은 GenericControlle을 만듭니다.

@SessionAttributes("model")
public abstract class GenericController<GS extends GenericService<E, S>, R, E, S> {

    @Autowired ApplicationContext applicationContext;

    GS service;
    R ref;

    Class<GS> serviceClass;
    Class<R> refClass;
    Class<E> entityClass;
    Class<S> searchParamClass;

    protected String baseUrl;

    public GenericController() {
        RequestMapping rm = this.getClass().getAnnotation(RequestMapping.class);
this.baseUrl = rm.value()[0];

        this.serviceClass = GenericUtils.getClassOfGenericTypeIn(getClass(), 0);
        this.refClass = GenericUtils.getClassOfGenericTypeIn(getClass(), 1);
        this.entityClass = GenericUtils.getClassOfGenericTypeIn(getClass(), 2);
        this.searchParamClass = GenericUtils.getClassOfGenericTypeIn(getClass(), 3);
    }

    @ModelAttribute("ref")
public R ref() {
return ref;
}

    @RequestMapping(value="/mgt")
    public void mgt(Model model){
        try {
            model.addAttribute("searchParam", searchParamClass.newInstance());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    @RequestMapping(method = RequestMethod.GET)
    public void list(Model model, S searchParam, PageParam pageParam) {
        model.addAttribute("list", service.list(pageParam, searchParam));
    }

    @RequestMapping(value = "/form", method = RequestMethod.GET)
    public String addForm(Model model){
        try {
            model.addAttribute("model", entityClass.newInstance());
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return baseUrl + "/new";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addFormSubmit(Model model, @Valid @ModelAttribute("model") E e, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return baseUrl + "/new";
        status.setComplete();
        service.add(e);
        return  "redirect:" + baseUrl + "/mgt";
    }

    @RequestMapping(value = "/{id}/form", method = RequestMethod.GET)
    public String editForm(@PathVariable int id, Model model){
        model.addAttribute("model", service.getById(id));
        return baseUrl + "/edit";
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.PUT)
    public String editFormSubmit(Model model, @Valid @ModelAttribute("model") E e, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return baseUrl + "/edit";
        status.setComplete();
        service.update(e);
        return  "redirect:" + baseUrl + "/mgt";
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.DELETE)
    public String delete(@PathVariable int id){
        service.deleteBy(id);
        return  "redirect:" + baseUrl + "/mgt";
    }

    @RequestMapping(value = "/{id}", method = RequestMethod.GET)
    public String view(@PathVariable int id, Model model){
        model.addAttribute("model", service.getById(id));
        return baseUrl + "/view";
    }

    @PostConstruct
    public void setUp(){
        this.service = applicationContext.getBean(serviceClass);
        this.ref = applicationContext.getBean(refClass);
    }

}

이전 글에서 만든 GenericUtils를 이용해서 타입 추론을 하고 그렇게 알아낸 타입을 사용해서 new Code() 하던 부분은 codeClass.newInstance()로 바꾸고, GS service = applicationContext.getBean(serviceClass) 이렇게 applicationContext에서 Class 타입으로 빈을 가져올 때 사용합니다.

필요한 타입은 4개 GenericService, Ref, Entity, SearchParam
Code 도메인 기준으로는 : CodeService, COdeRef, Code, CodeSearchParam이 필요합니다.

자 이제 CodeController 코드를 GenericController를 사용하도록 수정합니다.

@Controller
@RequestMapping("/base/code")
public class CodeController extends GenericController<CodeService, CodeRef, Code, CodeSearchParam> {

}

끝입니다. 맨 위에 있는 CodeController 코드와 비교하면.. 뭐. 거의 10배는 코드량을 줄인것 같네요. 이제 부터 찍어내는 일만... 아... 아니군요;;

뷰 코드까지 정리해야 찍어낼 수 있습니다. 컨트롤러까지만 만들면 뭐하나요. 화면이 안나오는데.. OTL...


top


[회사일] Generic 타입 추론 유틸 만들기

프로젝트/SLT : 2010.06.11 12:23


@Transactional
public class GenericServiceImpl<D extends GenericDao<E, S>, E, S> implements GenericService<E, S> {

    @Autowired ApplicationContext applicationContext;
    private Class<D> daoClass;
    protected D dao;

    public GenericServiceImpl(){
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[0];
        if (type instanceof ParameterizedType) {
            this.daoClass = (Class<D>) ((ParameterizedType) type).getRawType();
        } else {
            this.daoClass = (Class<D>) type;
        }
    }

...
}

지난 번에 작성했던 GenericService 코드입니다. 여기서 저 가운데 부분이 GenericDao나 GenericController에서 사용될 가능성이 높습니다. 사실 지금 GenericController를 작성하던 중이었는데 타입 추론할께 하두 많아서;; @_@  유틸로 빼고 있습니다.

구현은 간단합니다.

public class GenericUtils {

    public static Class getClassOfGenericTypeIn(Class clazz, int index){
        ParameterizedType genericSuperclass = (ParameterizedType) clazz.getGenericSuperclass();
        Type wantedClassType = genericSuperclass.getActualTypeArguments()[index];
        if (wantedClassType instanceof ParameterizedType) {
            return (Class) ((ParameterizedType) wantedClassType).getRawType();
        } else {
            return (Class) wantedClassType;
        }
    }

}

테스트도 해봐야겠죠.

public class GenericUtilsTest {

    @Test
    public void testGetClassOfGenericTypeIn() throws Exception {
        Class<String> stringClass = GenericUtils.getClassOfGenericTypeIn(SampleClass.class, 0);
        assertThat(stringClass.toString(), is("class java.lang.String"));

        Class<Map> mapClass = GenericUtils.getClassOfGenericTypeIn(SampleClass.class, 1);
        assertThat(mapClass.toString(), is("interface java.util.Map"));
    }

    class SampleGenericClass<S, M>{}
    class SampleClass extends SampleGenericClass<String, Map> {}
}

잘 되는군요. GenericController 마무리하러 가야겠습니다.


top


[Hot potatoes] 문제 만들기 프로그램

Good Tools : 2010.06.10 16:46


http://hotpot.uvic.ca/

흠냐.. 아내가 이걸로 학교 숙제를 해야된데서 잠깐 살펴봤는데 재밌더군요.ㅋㅋ
사용하기 쉽게 잘 만들었더라구요.

맥용도 있길래 써봤습니다. 왼쪽 감자들은 문제 유형이고 오른쪽은 감자로 만든 문제들을 하나의 문제집으로 묶어주는 녀석입니다.

문제 유형중에 JMatch를 써봤습니다.

이런식으로 왼쪽 오른쪽에 매치 되는걸 적어두고... 저장하고 export -> drop-down 형태로 만들었습니다. 결국엔 HTML이 만들어집니다.

매셔에서는 그렇게 만들어진 HTML을 가지고 index 페이지를 만들고 네비게이션을 정의해주느 것뿐이 없더군요. 간단합니다. 복잡하게 쓰려면 CSS도 변경 할 수 있고 HTML편집도 멋지게 하고 동영상, 이미지 추가해서 보여줄 수도 있습니다.

자 기본 디자인 가지고 만든 웹 페이지 입니다. 오른쪽에 있는 녀석들을 마우스 드래그 앤 드랍으로 연결할 수 있습니다.

http://dev.springsprout.org/nick.htm

캬캬캬캬. 봄싹 아이디 맞추기 고고씽.~



top


[집안일] 바느질하는 기선이



오늘은 왠지.. 쉬고 싶어서 회사에 가지 않았습니다. (오늘은 그래서 [회사일] 포스팅이 없을꺼에요.ㅋ)
초등학교 때 이후로 처음해보는 바느질.. 재밌더군요.


오전에 냉장고 위치 바꾸고, 커튼 다시 달고, 대문에 빛 가리게 달고, 청소까지.. 캬..
이제 아내가 해주는 밥먹고 씻고 공부해야지. 피아노도 쳐야겠군. 띵똥띵똥. 월광 1악장 ㄱㄱㅆ.
top

TAG 바느질

[회사일] GenericService 버그 수정하기

프로젝트/SLT : 2010.06.09 17:45


앞에서 만들었던 GenericService에는 버그가 있었습니다. 저걸 적용한 뒤에 애플리케이션을 실행해보니까 제대로 동작하지 않더군요. 제길... 테스트를 만들껄.. 후회했습니다.  대부분의 코드가 단순 위임이라고 해서 테스트를 무시하면 안됩니다. 사실 Service 코드에서 하는 일은 DAO로 단순 위임하는 코드밖에 없어 보이지만 그 이상으로 복잡합니다.

그 중 하나가 DAO를 주입 받는 일이죠. '주입 하는 일'도 아니고 '주입 받는 일'을 무시했네요. 

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/testContext.xml")
@Transactional
public class CodeServiceImplTest {

    @Autowired CodeService codeService;

    @Test
    public void di(){
        assertThat(codeService, is(notNullValue()));
    }

}

초간단 테스트를 만듭니다. codeService 자체를 DI 받을 수 있는지... 빈 팩토리에서 저 빈을 만들 수 있는지 확인합니다.

에러가 납니다.. ㅋ

Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException: No unique bean of type [osaf.dao.GenericDao] is defined: expected single matching bean but found 2: [codeDaoImpl, memberDaoImpl]
at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:779)
at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:686)
at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:478)
... 43 common frames omitted

핵심적인 에러 메시지는 이 부분이다. 뭔 내용인지는 해설하지 않겠습니다. 퀴즈 삼아 맞춰보시죠.

public class GenericServiceImpl<D extends GenericDao<E, S>, E, S> implements GenericService<E, S> {

    @Autowired ApplicationContext applicationContext;
    private Class<D> daoClass;

    GenericDao<E, S> dao;

    public GenericServiceImpl(){
        ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[0];
        if (type instanceof ParameterizedType) {
            this.daoClass = (Class<D>) ((ParameterizedType) type).getRawType();
        } else {
            this.daoClass = (Class<D>) type;
        }
    }

    public void add(E e) {
        dao.add(e);
    }

    public List<E> list(PageParam pageParam, S s) {
        pageParam.initCurrentPageInfosWith(dao.totalSize(s));
        return dao.list(pageParam, s);
    }

    public E getById(int id) {
        return dao.getById(id);
    }

    public void update(E e) {
        dao.update(e);
    }

    public void deleteBy(int id) {
        dao.deleteBy(id);
    }

    @PostConstruct
    public void setUpDao(){
        this.dao = this.applicationContext.getBean(daoClass);
    }
}

GenericService 구현체를 위와같이 수정했습니다. dao를 생성자에서 바로 연결하지 않고 @PostConstruct에서 연결한 것도 설명하지 않겠습니다. 요것도 심심하신 분 계시면 퀴즈 삼아 맞춰보시기 바랍니다.

퀴즈1. 에러가 난 원인은?
퀴즈2. 왜 생성자에서 applicationContext.getBean(daoClass)를 하면 안된느가?

top


[회사일] MemberService 만들기. GenericService 만들기

프로젝트/SLT : 2010.06.09 16:50


이번엔 바로 GenericService 인터페이스부터 만들죠.

public interface GenericService<E, S> {

    void add(E e);

    List<E> list(PageParam pageParam, S s);

    E getById(int id);

    void update(E e);

    void deleteBy(int id);
    
}

간단합니다. GenericDao랑 비슷하죠. 이 구현체도 간단합니다. 오히려 GenericDaoImpl 보다 훨씬더..

public class GenericServiceImpl<D extends GenericDao<E, S>, E, S> implements GenericService<E, S>{

    @Autowired protected D dao;

    public void add(E e) {
        dao.add(e);
    }

    public List<E> list(PageParam pageParam, S s) {
        pageParam.initCurrentPageInfosWith(dao.totalSize(s));
        return dao.list(pageParam, s);
    }

    public E getById(int id) {
        return dao.getById(id);
    }

    public void update(E e) {
        dao.update(e);
    }

    public void deleteBy(int id) {
        dao.deleteBy(id);
    }
    
}

유일하게 복잡한 부분이 저 위에 Generic 타입 선언한 부분인데.. GenericDao에 있는 메서드를 사용하려면 D라는 타입이 GenericDao를 확장한 녀석이라는걸 알려줘야 합니다. 그리고 GenericDAO에 넘겨주는 타입이 GenericService에서 사용할 타입과 같다는것도 알려줘야 해서 저렇게 복잡해졌습니다. 만야게 GenericDao 뒤에 붙인 <E, S>를 때어내면 dao.getById(id) 부분에서 타입 불일치 때문에 컴파일 에러가 떨어집니다.ㅋ

public interface MemberService extends GenericService<Member, MemberSearchParam>{

}

public class MemberServiceImpl extends GenericServiceImpl<MemberDao, Member, MemberSearchParam> implements MemberService{
    
}

위에서 만든걸 이용해서 만든 MemberService와 MemberServiceImpl 입니다. 캬.. 간단하군요.






top


[회사일] Member 추가. MemberDAO 구현. GenericDAO 구현

프로젝트/SLT : 2010.06.09 14:56


이제 Code 도메인 가지고 아주 기본적인 CRUD를 만들었으니 새로운 도메인을 하나 더 추가해서 중복코드를 잡아가면서 프레임워크를 뽑아내면 됩니다.

첫번쨰 도메인 CRUD를 만들때는 아키텍처 정하고 화면 구상하고 이것저것 플러그인 찾아보고 네비게이션 정하고 버튼 모양 덩덩 정하느라 시간이 많이 걸렸다면 이제부터는 중복코드와 싸움입니다.

먼저 Member 도메인을 추가합니다.

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    int id;

    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String loginId;
    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String password;

    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String name;
    @Column(length = 50)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    String email;
    @Column(length = 50)
    String phoneNumber;
    @Column(length = 50)
    String jobTitle;
    @Temporal(TemporalType.DATE)
    Date birthDay;
...
}

여기서도 애노테이션 뭉탱이를 중복제거하고 싶은데.. 일단 조금 뒤로 미루겠습니다.

다음은 DAO를 만듭니다. 인터페이스를 만들고 구현체를 만듭니다.

public interface MemberDao {

    void add(Member member);

    List<Member> list(PageParam pageParam, MemberSearchParam sp);

    int totalSize(MemberSearchParam sp);

    Member getById(int id);

    void deleteBy(int id);

    void update(Member member);

}

구현체는..

@Repository
public class MemberDaoImpl implements MemberDao {

    @Autowired SessionFactory sessionFactory;

    public void add(Member member) {
        getSession().save(member);
    }

    public Member getById(int id) {
        return (Member) getSession().get(Member.class, id);
    }

    public void deleteBy(int id) {
        int result = getSession().createQuery("delete from Member where id = ?").setInteger(0, id).executeUpdate();
        if(result != 1)
            throw new RuntimeException();
    }

    public void update(Member member) {
        getSession().update(member);
    }

    public List<Member> list(PageParam pageParam, MemberSearchParam searchParam) {
        Criteria c = getCriteriaOf(Member.class);
        //searching
        applySearchParam(c, searchParam);
        //paging
        c.setFirstResult(pageParam.getFirstRowNumber());
        c.setMaxResults(pageParam.getRows());
        //ordering
        if(pageParam.getSord().equals("asc"))
            c.addOrder(Order.asc(pageParam.getSidx()));
        else
            c.addOrder(Order.desc(pageParam.getSidx()));

        return c.list();
    }


    public int totalSize(MemberSearchParam searchParam) {
        Criteria c = getCriteriaOf(Member.class);
        applySearchParam(c, searchParam);
        return (Integer)c.setProjection(Projections.count("id"))
            .uniqueResult();
    }

    private void applySearchParam(Criteria c, MemberSearchParam searchParam) {
        
    }

    private Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    private Criteria getCriteriaOf(Class clazz){
        return getSession().createCriteria(clazz);
    }
        
}

CodeDaoImple 코드와 거의 똑같습니다.

상속을 사용해서 중복을 제거하겠습니다. 먼저 인터페이스 부터..

public interface GenericDao<E, S> {

    void add(E e);

    List<E> list(PageParam pageParam, S s);

    int totalSize(S s);

    E getById(int id);

    void deleteBy(int id);

    void update(E e);

}

그리고 이걸 CodeDao와 MemberDao에 사용합니다.

public interface CodeDao extends GenericDao<Code, CodeSearchParam>{
    
}

public interface MemberDao extends GenericDao<Member, MemberSearchParam>{
   
}

오퀘 인터페이스가 텅 비었습니다. 아주 일반적인 CRUD 이외 추가기능이 생기면 눈에 확 띄겠죠.

다음은 GenericDao 구현체를 만듭니다.

public abstract class GenericDaoImpl<E, S> implements GenericDao<E, S>{

    @Autowired protected SessionFactory sessionFactory;
    
    private Class<E> entityClass;

    public GenericDaoImpl() {
ParameterizedType genericSuperclass = (ParameterizedType) getClass().getGenericSuperclass();
        Type type = genericSuperclass.getActualTypeArguments()[0];
        if (type instanceof ParameterizedType) {
            this.entityClass = (Class) ((ParameterizedType) type).getRawType();
        } else {
            this.entityClass = (Class) type;
        }
}
    
    public void add(E e) {
        getSession().save(e);
    }

    public E getById(int id) {
         return (E) getSession().get(entityClass, id);
    }

    public void deleteBy(int id) {
        int result = getSession().createQuery("delete from" + entityClass.getSimpleName()+ " where id = ?").setInteger(0, id).executeUpdate();
        if(result != 1)
            throw new RuntimeException();
    }

    public void update(E e) {
        getSession().update(e);
    }

    public List<E> list(PageParam pageParam, S s) {
        Criteria c = getCriteriaOf(entityClass);
        //searching
        applySearchParam(c, s);
        //paging
        c.setFirstResult(pageParam.getFirstRowNumber());
        c.setMaxResults(pageParam.getRows());
        //ordering
        if(pageParam.getSord() != null){
            if (pageParam.getSord().equals("asc"))
                c.addOrder(Order.asc(pageParam.getSidx()));
            else
                c.addOrder(Order.desc(pageParam.getSidx()));
        }
        //noinspection unchecked
        return c.list();

    }

    protected abstract void applySearchParam(Criteria c, S s);

    public int totalSize(S s) {
        Criteria c = getCriteriaOf(entityClass);
        applySearchParam(c, s);
        return (Integer) c.setProjection(Projections.count("id"))
                .uniqueResult();
    }

    protected Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    protected Criteria getCriteriaOf(Class clazz){
        return getSession().createCriteria(clazz);
    }
}

그리고 이녀석을 사용하도록 CodeDaoImpl과 MemberDaoImpl을 수정합니다.

@Repository
public class CodeDaoImpl extends GenericDaoImpl<Code, CodeSearchParam> implements CodeDao {
    
    @Override
    protected void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "code", searchParam.getCode());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
        CriteriaUtils.addOptionalEqual(c, "cate", searchParam.getCateValue());
    }
    
}

@Repository
public class MemberDaoImpl extends GenericDaoImpl<Member, MemberSearchParam> implements MemberDao {

    @Override
    protected void applySearchParam(Criteria c, MemberSearchParam searchParam) {

    }
}

마지막으로 이전에 만들어 둔 CodeDaoImplTest를 한번 돌려주면 됩니다. 굳이 뭐또 GenericDaoImpl 테스트를 만들 필요는 없는것 같네요.

이렇게 과감한 코드 수정을 할 수 있었던 게 다 CodeDaoImplTest 덕분입니다.

자 그럼 잠깐 밥먹고 MemberService도 만들어야지 





top


[회사일] CriteriaUtils 테스트하기

프로젝트/SLT : 2010.06.09 12:28


먼저 Criteria가 필요한데.. 애매합니다. 하이버네이트 Session이 만들어주는 객체인지라... 흠.. 난 Criateria나 Session을 테스트하고 싶은 생각은 없고 CriateriaUtils이 제대로 동작하는지만 확인하면 되는데 말이죠.

public class CriteriaUtils {
    public static void addOptionalLike(Criteria c, String fieldName, String value) {
        if(StringUtils.isEmpty(value)){
            c.add(Restrictions.ilike(fieldName, value, MatchMode.ANYWHERE));
        }    
    }

    public static void addOptionalEqual(Criteria c, String fieldName, Integer value) {
        if(value != null && value != 0){
            c.add(Restrictions.eq(fieldName, value));
        }
    }
}

이런코드가 있으니.. 조건이 잘 먹는지 제가 원하느대로 조건문을 쓴게 맞는지 확인하고 싶었습니다. 이럴때 사용할 수 있는게 목킹 프레임워죠. EasyMock이나 JMock, Mockito 등이 있는데 저는 Mockito가 편해서 이걸 쓰기로 했습니다.

검색해보니 EasyMock을 사용해서 Criteria를 사용하는 DAO를 테스트한 글이 나옵니다.
http://toby.epril.com/?p=195

저는 EasyMock대신 Mockito를 사용했고, DAO 대신 CriteriaUtils를 테스트했습니다.

public class CriteriaUtilsTest {

    @Test
    public void testAddOptionalLike() throws Exception {
        Criteria c = mock(Criteria.class);
        CriteriaUtils.addOptionalLike(c, "name", "whiteship");
        verify(c).add(any(Criterion.class));

        reset(c);
        CriteriaUtils.addOptionalLike(c, "name", null);
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        CriteriaUtils.addOptionalLike(c, "name", "");
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        CriteriaUtils.addOptionalLike(c, "name", "   ");
        verify(c, times(0)).add(any(Criterion.class));
    }

    @Test
    public void testAddOptionalEqual() throws Exception {
        Criteria c = mock(Criteria.class);
        CriteriaUtils.addOptionalEqual(c, "age", 1);
        verify(c).add(any(Criterion.class));

        reset(c);
        CriteriaUtils.addOptionalEqual(c, "age", null);
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        CriteriaUtils.addOptionalEqual(c, "age", 0);
        verify(c, times(0)).add(any(Criterion.class));
    }

}

웃긴건,... any(Criterion.class) 대신에 구체적인 예상 값이 add(Restrictions.ilke("name", "whiteship", MatchMode.ANYWHERE))를 사용하면 결과가... 좀.. @_@

        Criteria c = mock(Criteria.class);
        CriteriaUtils.addOptionalLike(c, "name", "whiteship");
        verify(c).add(Restrictions.ilike("name", "whiteship", MatchMode.ANYWHERE));

즉 이렇게 작성하면

Argument(s) are different! Wanted:
criteria.add(name ilike %whiteship%);
-> at osaf.util.CriteriaUtilsTest.testAddOptionalLike(CriteriaUtilsTest.java:25)
Actual invocation has different arguments:
criteria.add(name ilike %whiteship%);
-> at osaf.util.CriteriaUtils.addOptionalLike(CriteriaUtils.java:16)

Expected :criteria.add(name ilike %whiteship%);
Actual   :criteria.add(name ilike %whiteship%);

at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method)
at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:39)
at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:27)
at java.lang.reflect.Constructor.newInstance(Constructor.java:513)
at osaf.util.CriteriaUtilsTest.testAddOptionalLike(CriteriaUtilsTest.java:25)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:25)
at org.junit.runners.model.FrameworkMethod$1.runReflectiveCall(FrameworkMethod.java:44)
at org.junit.internal.runners.model.ReflectiveCallable.run(ReflectiveCallable.java:15)
at org.junit.runners.model.FrameworkMethod.invokeExplosively(FrameworkMethod.java:41)
at org.junit.internal.runners.statements.InvokeMethod.evaluate(InvokeMethod.java:20)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:76)
at org.junit.runners.BlockJUnit4ClassRunner.runChild(BlockJUnit4ClassRunner.java:50)
at org.junit.runners.ParentRunner$3.run(ParentRunner.java:193)
at org.junit.runners.ParentRunner$1.schedule(ParentRunner.java:52)
at org.junit.runners.ParentRunner.runChildren(ParentRunner.java:191)
at org.junit.runners.ParentRunner.access$000(ParentRunner.java:42)
at org.junit.runners.ParentRunner$2.evaluate(ParentRunner.java:184)
at org.junit.runners.ParentRunner.run(ParentRunner.java:236)
at org.junit.runner.JUnitCore.run(JUnitCore.java:157)
at com.intellij.junit4.JUnit4IdeaTestRunner.startRunnerWithArgs(JUnit4IdeaTestRunner.java:94)
at com.intellij.rt.execution.junit.JUnitStarter.prepareStreamsAndStart(JUnitStarter.java:165)
at com.intellij.rt.execution.junit.JUnitStarter.main(JUnitStarter.java:60)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:39)
at com.intellij.rt.execution.application.AppMain.main(AppMain.java:110)

이런 에러가 나는데.. 도무지 콘솔을 봐도 모르겠습니다. 콘솔에 찍힌게 같은데.. 다르다니?? 먼소리람.. 어차피 내가 원하건 Criateria에 add()가 호출되는냐 마느냐 였으니까 그냥 any()로 해결했습니다.
top


[회사일] 검색에 enum 필드 추가하기

프로젝트/SLT : 2010.06.09 11:47


검색 파라미터에 추가해주고 화면에 추가하고 DAO에서 검색부분 코드만 조금 바꾸면 될 것 같습니다.

코드 검색 파라미터를 하나의 클래스로 정의해놨기에 망정이지 이걸 개별적으로 다 핸들러 파라미터로 받아서 처리하면;;; @_@;; 컨트롤러 코드도 손대야 했을껍니다. 아마 서비스 코드도 마찬가지구요.

public class CodeSearchParam {

    private int cateValue;

    private String name;

    private String code;

...
}

이렇게 CodeCate의 값을 바로 가져옵니다. 스프링 바인딩 사용해서 CodeCate 타입으로 받아도 되지만 굳이 그럴필요가 없더군요. 어차피 DB에 들어있는 값과 비교하려면 CodeCate의 value가 필요하며 화면에서도 이걸 전달해 주는데 굳이 이걸 또 포매터 사용해서 CodeCate로 바꿔서 거기서 .getValue()로 꺼내서 비교할 필요가 있나 싶더라구요. 그래서 그냥 int 타입인 cateValue를 선언했습니다.

이제 화면코드로 넘어갑니다.

                <p class="ui-widget-content"><label>코드종류: </label>
                    <form:select path="cateValue">
                        <form:option value="0" label="ALL"/>
                        <form:options items="${ref.codeCateList}" itemLabel="descr" itemValue="value"/>
                    </form:select>
                </p>

아무것도 선택하지 않았을 때 기본으로 ALL이 보일테고 그걸 선택하면 0이라는 값이 넘어가게 합니다. 뭐 이부분은 맘대로인것 같은데 그닦 맘에 들진 않아도 뭐 어쩔 수 있나요. 모든 enum마다 ALL 이라는 값을 만들기도 뭐하고.. 걍 이대로가 나은것 같습니다.

DAO에서 검색 파라메터를 적용하는 부분에 코드를 추가합니다.

    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "code", searchParam.getCode());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
        CriteriaUtils.addOptionalEqual(c, "cate", searchParam.getCateValue());
    }

CriteriaUtils에 메서드를 하나 추가해 줍니다.

    public static void addOptionalEqual(Criteria c, String fieldName, Integer value) {
        if(value != null && value != 0){
            c.add(Restrictions.eq(fieldName, value));
        }
    }

끝. 오예 잘 됩니다.

컨트롤러나 서비스 코드는 한줄도 건드리지 않았답니다. 
한번 만들어 논 뒤로는 거의 본적이 없으니 어떻게 생겼는지도 기억나지 않는군요.


top


[JSP 에디터] 인텔리J는 정말 똑똑해.

Good Tools : 2010.06.09 11:08


오전에 다른일을 좀 하다가 슬슬 회사일을 하려고 코드 패키지 정리좀 하고 검색 쪽 코딩을 시작하는데... 

깜짝아!!!


무심코 누른 자동완성 키에 모델 정보가 튀어나오다니;;; 세상에.. 좋아 좋아!!
top


[회사일] Enum 추가, Formatter 적용

프로젝트/SLT : 2010.06.08 17:47


Code 도메인에 enum으로 CodeCate 타입 필드를 하나 추가합니다. Enum을 사용할 땐 DB에 저장될 값과 화면에 보여줄 값과 순서를 지정하는게 보통인지라 그런 기능을 갖추게 하는 인터페이스를 하나 정의했었습니다.

public interface PersistentEnum {

String getDescr();

Integer getValue();

int getOrder();

}

enum이 이 인터페이스를 구현하도록 만듭니다. enum에는 상속다운 상속이 없어서 매번 똑같은 코드가 반복되지만 그래도 머.. 필요하기 때문에 어쩔 수 없습니다.

public enum CodeCate implements PersistentEnum {

COLOR(10, "색상", 2),
SEX(15, "성별", 3),
SIZE(20, "사이즈", 1);

private final Integer value;
private final String descr;
private final int order;

private CodeCate(Integer value, String descr, int order) {
this.value = value;
this.descr = descr;
this.order = order;
}

public Integer getValue() {
return value;
}
public String getDescr() {
return descr;
}
public int getOrder() {
return order;
}

}

그리고 하이버네이트에서 이 타입을 알 수 있게 UserType을 만들어줘야 하는데 GenericPesistentEnumUserType을 사용해서 간단하게 만들 수 있습니다.

public class CodeCateUserType extends GenericEnumUserType<CodeCate> implements Serializable {
    
}

이게 끝입니다. Code 도메인에 필드와 매핑 정보를 추가합니다.

        @Column
@Type(type="smdis.domain.usertype.CodeCateUserType")
private CodeCate cate;

위에서 만든 UserType을 지정해주면 됩니다.

여기까지 한다음 웹쪽을 생각해보죠.

새 코드를 입력할 때 코드 카테고리를 선택하려면 코드 카테 목록이 model에 들어있고 화면에서 그 값을 참조할 수 있어야 합니다. 이런 참조형 데이터를 한곳에 모아서 화면에 전달하도록 하죠.

@Component
public class CodeRef {

    public List<CodeCate> getCodeCateList(){
        return PersistentEnumUtil.sortedListOf(CodeCate.class);
    }
}

그리고 CodeController에는 @ModelAttribute를 사용해서 화면에 전달합니다.

        @Autowired CodeRef ref;

        @ModelAttribute("ref")
public CodeRef ref() {
return ref;
}

그럼 이제 화면에서 참조할 수 있죠.

<p class="ui-widget-content"><label>코드종류</label><form:select path="cate" items="${ref.codeCateList}" itemValue="value" itemLabel="descr" /><form:errors path="cate" cssClass="smdis-error-message"/></p>
                
스프링 form 태그를 이용해서 Enum 타입 List를 items에 넘겨주고 그 List 엔티티의 필드 중에서 값이 될 필드와 레이블이 될 필드명을 설정해 줍니다.


그럼 이렇게 화면에 보이는 값는 PersistentEnum의 getDescr() 값으로 보여지고 실제 선택했을때 핸들러로 넘어가게 되는 값은 getValue() 값이 됩니다.

이제 적당한 값을 넣고 저장을 누르면..


이렇게 됩니다. 왜 이런 에러가 나는지는 아시겠죠? 필요한건 CodeCate 타입인데 20이라는 String 타입이 넘어와서 도무지 어떻게 바인딩해야할지 모르겠다고 에러가 나는겁니다. 해결책은 간단합니다. 알려주면 됩니다. 어떻게 바인딩 하면 되는지 스프링한테..

이전까진 PropertyEditor를 사용하던지 아님 직접 핸들러에서 매번 request에서 파라메터 받아서 처리하던지 해야했지만 스프링 3.0에서는 Converter와 Formatter라는게 추가됐습니다. 그 중에서도 웹 용으로는 특수화된 Formatter를 사용하겠습니다.

이전 글에서 만든 GenericPersistentEnumFormatter를 사용해서 만듭니다.

@Component
public class CodeCateFormatter extends GenericPersistentEnumFormatter<CodeCate> {
}

쓰레드 세이프하기 때문에 빈으로 등록해서 싱글톤으로 써도 됩니다. PropertyEditor는 2단계 호출을 거치기 때문에 그다지 안전하지 않았습니다. 매번 new를 사용해서 등록해주는것이 안전했었죠. 이젠 그럴필요가 없으니까 이렇게 합니다.

그리고 이제 이 포매터를 등록하는 일이 남았는데 조금 귀찮습니다;

public class SmdisFormattingConversionServiceFactoryBean extends FormattingConversionServiceFactoryBean {

    @Autowired CodeCateFormatter codeCateFormatter;

    @Override
    protected void installFormatters(FormatterRegistry registry) {
        super.installFormatters(registry);
        registry.addFormatterForFieldType(CodeCate.class, codeCateFormatter);
    }
}

먼저 이렇게 FormattingConversionServiceFactoryBean를 상속한 다음에 installFormatters()를 재정의해서 원하는 포매터를 등록해 줍니다.

마지막으로 이녀석을 conversionService로 등록하고 어댑터에 끼워줘야 합니다. 그나마 이부분은 스프링 3.0에 추가된 mvc 네임스페이스 덕분에 간단해 졌습니다.,

    <mvc:annotation-driven conversion-service="conversionService" />

    <bean id="conversionService" class="smdis.common.formatter.SmdisFormattingConversionServiceFactoryBean"/>

끝.. 이제는 스프링이 CodeCate 타입을 어떻게 바인딩 해야하는지 알고 있기 떄문에 잘 들어갑니다.


내일은 검색과 그리드쪽에도 CodeCate를 추가해야겠군요. 
흠.. 그리드는 간단하니깐 걍 오늘 추가해야겠네요.

top


[회사일] GenericPersistentEnumFormatter 만들기

프로젝트/SLT : 2010.06.08 16:40


GenericPropertyEditor도 만들었었는데 Formatter라고 못만들까 하는 생각에 만들어봤습니다. Locale은 무시해서;; 반쪽짜리긴 하지만 그래도 쓸만합니다.

포매터를 만들 대상은 PersuistentEnum 입니다. 자세한 내용은 이전 글에서 확인하시기 바랍니다.

http://whiteship.me/search/PersistentEnum

간단히.. Enum을 사용할 떄 필요한 기능을 정의한 PersistentEnum 인터페이스를 사용할 때 필요한 UserType이나 Formatter를 쉽게 만들 수 있게 GenericPersistentEnumUserType GenericPersistentEnumProperyEditor등을 만들었습니다.

public class PersistentEnumFormatter<E extends PersistentEnum> implements Formatter<E> {

    protected Class<E> enumClass;

@SuppressWarnings("unchecked")
public PersistentEnumFormatter() {
ParameterizedType genericSuperclass = (ParameterizedType) getClass()
.getGenericSuperclass();
Type type = genericSuperclass.getActualTypeArguments()[0];
if (type instanceof ParameterizedType) {
this.enumClass = (Class) ((ParameterizedType) type).getRawType();
} else {
this.enumClass = (Class) type;
}
}
    
    public String print(E object, Locale locale) {
        return object.getDescr();
    }

    public E parse(String text, Locale locale) throws ParseException {
        return PersistentEnumUtil.valueOf(enumClass, Integer.parseInt(text));
    }
}

제네릭 타입이 필요해서 생성자에서 타입 추론을 사용했는데 그 코드만 빼면 너무도 간단합니다. PE 만들때랑 비슷하거나 더 쉬운것 같습니다.

그래도 왠지 불안하니깐 테스트 해봤습니다.

public class PersistentEnumFormatterTest {

    CodeCateFormatter ccf;

    @Before
    public void setUp(){
        ccf = new CodeCateFormatter();
    }

    @Test
    public void testPrint() throws Exception {
        assertThat(ccf.print(CodeCate.SIZE, null), is("사이즈"));
    }

    @Test
    public void testParse() throws Exception {
        assertThat(ccf.parse("10", null), is(CodeCate.COLOR));
    }

    @Test
    public void testParseNotInValue() throws Exception {
        assertThat(ccf.parse("0", null), is(nullValue()));
    }

    @Test(expected = NumberFormatException.class)
    public void testParseWrongValue() throws Exception {
        assertThat(ccf.parse("캐스팅에러", null), is(nullValue()));
    }

    class CodeCateFormatter extends PersistentEnumFormatter<CodeCate> {

    }

}

흠냐 이제 포매터로 갈아탈 시간이 됐군요. 
어서 CodeCate 이늄(enum)좀 추가하고 마무리 해야겠어요.
top


[회사일] CRUD 화면 디자인 수정

프로젝트/SLT : 2010.06.08 12:37



목록에서 + 버튼을 누르면 추가 팝업이 뜨고 만약 거기서 잘 입력하면 다시 팝업이 닫히면서 그리드가 갱신 됩니다. 만약 이때 엉뚱한 값을 입력해서 검증 에러가 발생하면 팝업이 아니라 전체 메뉴 속에서 에러와 함께 폼을 보여줍니다.

그리드 목록 중 하나를 더블클릭하면 뷰 화면으로 이동하고 여기서 수정 화면으로 넘어가거나 삭제를 할 수 있습니다.

수정화면에서는 내용을 편집할 수 있고 저장, 삭제를 할 수 있습니다. 또한 다시 원래 값으로 되돌릴수도 있고 목록으로 돌아가거나 뷰 화면으로 돌아갈 수도 있습니다.

네비게이션 관련 버튼은 오른쪽에 두었고 기능 버튼은 왼쪽에 두었습니다.

이제 Enum을 추가해서 CodeCate를 선택할 수 있게 해야겠습니다.
top


봄싹 마니산 등정 성공



마니산 최악의 코스(맵에도 없는 코스)를 선택한 봄싹은 등산 초반 부터 시작된 고난을 이겨내고 장작 4시간 반만에 드디어 마니산 정상인 첨성단에 도착합니다.

그리고 저는 아이스크림을 먹었습니다. 그것도 두개 씩이나.. +_+



올해 먹었던 아이스크림 중에 제일 맛있는 아이스크림이었어요.

올라갈 때 선택한 코스는 택시 기사 아저씨가 추천해준 코스인데 입장료를 내지 않아도 되고 2시간 반이면 된다고 해서... 갔지만.. 정말 막막하더군요. 이정표도 없고 이길이 맞는 길인지도 잘 모르겠고, 그냥 '높은곳으로 가자. 계속 올라가다 보면 정상에서 만나겠지' 라는 일념으로 계속해서 올라갔습니다.

그렇게 오르기를 계속하다가 처음 만난 이정표가 얼마나 반갑던지.. '아.. 다행이다.' 라는 생각이 들더군요. 계속 가면 정상은 나온다는거니깐 안심했습니다. 그러나.. 예상보다 험하고 길어지는 산행에 당황하지 않을 수가 없더군요. 그래도 어쩔 수 없었습니다. 그 상태에서 다시 올라온 길을 내려가기는 싫고 끝까지 올라가는 수밖에 없다는 생각으로 계속 올라갔습니다.

이번 산행에서는 봄싹 팀원들의 팀웍을 보는 재미가 쏠쏠 했습니다. 우선 운영진의 불찰로 인해 엄한 등산로를 타기 시작했는데도 모두 끝까지 포기하지 않고 올라갔다는 것이 저로써는 굉장히 기뻤습니다. 운영진이 매번 잘할 수는 없습니다. 운영진도 사람들인지라 실수도 하고 잘못된 방향으로 이끌기도 하지만 계속해서 운영진을 믿고 따라 줘야만 잘못도 인지하고  더 나은 방향으로 더 성숙한 운영을 할 수 있기 때문입니다. (이번 일은 운영진이 사과드릴만한 행동을 했습니다. 흑흑. 다음부턴 꼭 사전답사도 제대로 하고 정확한 경로로만 다니겠습니다.) 또한 봄싹 내부에서 힘든 사람 짐을 들어주고 쳐지는 사람을 앞으로 보내고 중간 중간 선두와 후미 간격 조정, 물 나눠 마시기 등을 보면서 상당히 즐거웠습니다. '아.. 다행이다. 봄싹이 이래서 계속 유지 될 수 있는 거구나..' 라는 생각이 들었고, 따뜸한 한마디를 아끼지 않던 정우형도 고마웠습니다. 서로가 어찌나 이렇게 잘 맞물리는지.. 후훗. 머 가끔 모난 곳도 있긴하지만 바위가 세월에 깎이듯 같이 오래 지내다 보면 분명히 서로 잘 어울리게 될 거라 생각합니다.

이번 봄싹 MT는 힘들었던만큼 좋은 추억거리가 된 것 같습니다.
다음에도 좋은 추억 만들 수 있게 열심히 달립시다.


한분은 비공개를 요청하셨기 때문에 고스트로 처리해드렸습니다.ㅋㅋㅋ


 







top


[회사일] CRUD 구현

프로젝트/SLT : 2010.06.07 16:42


먼저 앞에서 만든 뷰를 가지고 컨트롤러 코드를 만드는데 이때 URL을 어떻게 가져갈지 구상해야합니다.

/base/code/list GET
/base/code/add GET 
/base/code/add POST
/base/code/update?id=23 GET 
/base/code/update?id=23 POST 
/base/code/delete?id=23 GET 

이전에는 이런식으로 썼었습니다. 그런데 이번에는 URL을 REST 스타일로 만들어 보고자 스프링 Roo 레퍼런스에 있는 그림을 참조했습니다.

http://static.springsource.org/spring-roo/reference/html/beginning.html


그래서 이번에는

/base/code/ GET
/base/code/form GET
/base/code POST
/base/code/{id}/form GET
/base/code/{id} PUT
/base/code/{id} DELETE

이런식으로 구성했습니다. 아직 하나 안만든건 view인데 그건 /base/code/{id} GET이 될겁니다. 

아직 익숙하지 않아서 잘 적응되고 있진 않은데 이렇게 만들어 두면 나중에 웹 서비스 별도로 만들 필요없이 저 런 RESTful URL과 스프링 3.0의 컨텐츠 네고 기능을 이용해서 모바일 용 앱을 만들때 한결 편할 것 같습니다.

암튼 이 다음은 스프링 3.0의 @Validation 기능을 적용하는 것인데 이때 하이버 벨리데이터를 의존성에 추가해 줬습니다.

        <!-- Validation -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>com.springsource.org.hibernate.validator</artifactId>
            <version>4.0.0.GA</version>
        </dependency>
        <dependency>
            <groupId>javax.xml.bind</groupId>
            <artifactId>com.springsource.javax.xml.bind</artifactId>
            <version>2.1.7</version>
        </dependency>

스프링 웹 설정은 변경한것이 없습니다.

<mvc:annotation-driven />

이게 전부이고 나머진 예외-뷰 설정과 컨텐츠 네고 설정 뿐입니다.

@Controller
@RequestMapping("/base/code/*")
@SessionAttributes("code")
public class CodeController {

    @Autowired CodeService codeService;

    @RequestMapping(value="mgt")
    public void mgt(Model model){
        model.addAttribute("searchParam", new CodeSearchParam());
    }

    @RequestMapping(method = RequestMethod.GET)
    public void list(Model model, CodeSearchParam searchParam, PageParam pageParam) {
        model.addAttribute("codeList", codeService.list(pageParam, searchParam));
    }

    @RequestMapping(value = "form", method = RequestMethod.GET)
    public String addForm(Model model){
        model.addAttribute("code", new Code());
        return "/base/code/new";
    }

    @RequestMapping(method = RequestMethod.POST)
    public String addFormSubmit(Model model, @Valid Code code, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return "base/code/new";
        status.setComplete();
        codeService.add(code);
        return  "redirect:/base/code/mgt";
    }

    @RequestMapping(value = "{id}/form", method = RequestMethod.GET)
    public String editForm(@PathVariable int id, Model model){
        model.addAttribute("code", codeService.getById(id));
        return "/base/code/edit";
    }

    @RequestMapping(value = "{id}", method = RequestMethod.PUT)
    public String editFormSubmit(Model model, @Valid Code code, BindingResult result, SessionStatus status){
        if(result.hasErrors())
            return "base/code/edit";
        status.setComplete();
        codeService.update(code);
        return  "redirect:/base/code/mgt";
    }

    @RequestMapping(value = "{id}", method = RequestMethod.DELETE)
    public String delete(@PathVariable int id){
        codeService.deleteBy(id);
        return  "redirect:/base/code/mgt";
    }

}

컨트롤러는 이렇게 구현했는데 URL을 클래스 선언부와 메서드 선언부에 분할해서 설정했습니다. 그래야 나중에 상위클래스로 빼서 프레임워크화 할 때 용이하기 때문입니다. 서비스와 DAO 코드는 간단하기 때문에 생략입니다.

참 컨트롤러에서 PUT, DELETE 같은 method를 사용하고 있는데 사실은 화면에서 넘어올땐 걍 POST일 뿐이고 _method라는 히든 파람을 스프링이 읽어서 적절한 애노테이션 핸들러를 찾아주게 되어 있습니다. 히든 파람으로 RESTfult 한 API를 만들기 위해서는 web.xml에 필터를 하나 등록해줘야 합니다.

<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

자 그럼 오늘은 이만하고 내일은 Code에 새로운 속성들을 추가해 봐야겠습니다. 시간이 남으면 User 도메인 CRUD까지 구현해야겠네요. 그 다음부터는 드뎌 OSAF 3.0 코드가 조금씩 간추려 지겠군요.


top


[회사일] add 팝업 만들기

프로젝트/SLT : 2010.06.07 12:17



저 쬐끄만 팝업창으로 영.. 맘에 드는 플러그인이 없어서 고르고 고르다 찾은게..

http://colorpowered.com/colorbox/

이거.. 더이상 찾기도 힘들고 디자인 하기도 귀찮으니 이것으로.. 끝.

new.jsp 페이지를 별도로 만들고, mgt.jsp 페이지에서 + 버튼을 누르면 new.jsp를 저런 형태로 보여주도록 만들었습니다.

mgt.jsp

 $("#smdis-new-button").colorbox({href:"/base/code/new"});

버튼 모양은 jquery ui의 button()을 이용해서 꾸몄습니다. 이제 화면은 만들어졌으니.. 구현으로..


top

TAG add 뷰

[스프링 퀴즈] @Autowired

Spring/etc : 2010.06.04 17:30


@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class AutowiredTest {

    @Autowired DataSource dataSource;

    @Test
    public void notNull(){
        assertThat(dataSource, is(notNullValue()));
    }

}

위와 같은 테스트가 있다. 위 코드가 있는 곳과 같은 패키지에 AutowiredTest-context.xml 이라는 빈 설정 파일을 만들었다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="org.springframework.jdbc.datasource.SimpleDriverDataSource"/>

</beans>

1. 이렇게 설정했을 때 테스트는 어떻게 될까? 깨질까? 성공할까?

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean class="org.springframework.jdbc.datasource.SimpleDriverDataSource"/>

    <bean class="org.springframework.jdbc.datasource.SimpleDriverDataSource"/>

</beans>

2. 이렇게 설정했다면 어떻게 될까?

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource"/>

    <bean id="testDataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource"/>

</beans>

3. 그럼 이건 어떨까?

셋다 맞춘 분에게 물어보고 싶은 질문이 있는데;; 맞추시는 분께 댓글로 질문 드리겠습니다.
top


[회사일] 그리드 정렬 기능 구현하기

프로젝트/SLT : 2010.06.03 11:47


이제 실제 회사일과 싱크 맞는다. 휴.. 이전까지 싱크 맞출려고 급하게 달렸더니 정신 없다.ㅋ

그리드를 누르면 JqGrid가 알아서 매개변수에 정렬할 필드 명과 정렬 방향을 넘겨준다. 그럼 컨트롤러에서 잘 받는지 확인부터 해볼까.

    @RequestMapping
    public void list(Model model, CodeSearchParam searchParam, PageParam pageParam) {
        System.out.println("sidx: " + pageParam.getSidx());
        System.out.println("sord: " + pageParam.getSord());

        model.addAttribute("codeList", codeService.list(pageParam, searchParam));
    }

잠깐 저렇게 추가해서 콘솔에 찍고나서 다시 지운다. 이럴때는 JRebel이 빛을 발한다. 짱이다. 서버 껐다켜지 않아도 이정도는 간단하게 잘 처리해준다.

서비스 코드 고칠것 없다. 이미 PageParam에 들어있으니.. 그냥 타고 갈것다.
DAO 인터페이스도 고칠것 없다. DAO 구현체의 list만 고치면 될 것 같다.

    public List<Code> list(PageParam pageParam, CodeSearchParam searchParam) {
        Criteria c = getCriteriaOf(Code.class);
        //searching
        applySearchParam(c, searchParam);
        //paging
        c.setFirstResult(pageParam.getFirstRowNumber());
        c.setMaxResults(pageParam.getRows());
        //ordering
        if(pageParam.getSord().equals("asc"))
            c.addOrder(Order.asc(pageParam.getSidx()));
        else
            c.addOrder(Order.desc(pageParam.getSidx()));
        
        return c.list();
    }

고쳤다. 이전에 만들었던 테스트를 돌렸다. 혹시 이 코드를 수정하면서 이전에 되던게 안될까바 걱정되서 돌려봤다. 이런 테스트를 리그레션 테스트 또는 회기 테스트라는 어려운 말을 쓰기도 하는데 뭐.. 많이 아는척하는 사람들과의 대화에서 밀리지 않으려면 알아두는게 좋겠다.

자 그럼 이제 다시 지금 추가한 코드에 대한 테스트를 만들어보자.

    @Test
    public void testListOrdering() throws Exception {
        insertXmlData("testData2.xml");

        CodeSearchParam codeSearchParam = new CodeSearchParam();
        PageParam pageParam = new PageParam();
        pageParam.setRows(5);
        pageParam.setPage(1);
        pageParam.setSidx("id");  // id 필드 기준으로
        pageParam.setSord("asc"); // 내림차순

        List<Code> codeList = codeDao.list(pageParam, codeSearchParam);
        String result = "";
        for(Code code : codeList){
            result += code.getId();
        }
        assertThat(result, is("12345"));

        pageParam.setSidx("id");  // id 필드 기준으로
        pageParam.setSord("desc"); // 내림차순

        codeList = codeDao.list(pageParam, codeSearchParam);
        result = "";
        for(Code code : codeList){
            result += code.getId();
        }
        assertThat(result, is("76543"));
    }

오퀘 잘 돈다. 캬캬 사실 맨 마지막 줄은 예상을 잘못해서 "54321"로 적었었는데.. 테스트 코드가 니보다 똑똑했다. @_@;;; 아... 이런!..

테스트보다 멍청한.. 개발자.. 이게 나다. ㅠ.ㅠ

흠.. 슬슬 그리드 쪽이 정리가 되가니깐 추가/수정 기능쪽으로 넘어갈까 말까.. 
아니면 기본 정렬 기능을 넣을까..고민 된다.

사실 대표님은 가끔 "일단 CodeCate로 정렬하고 그 다음에 코드값으로 정렬해줘" 이렇게 다중 정렬을 요구한적이 있었다.

밥먹으면서 생각해봐야지.. 그전까진 잠깐 놀까나..
top


[회사일] 검색및 페이지 처리 구현하기

프로젝트/SLT : 2010.06.03 11:16


사실 이 작업은 따로 따로 했지만 중간에 정리해 두지 않아서 다시 코드를 빼면서 정리하기는 귀찮아서 한번에 정리한다.

먼저 화면에서 요청을 보내도록 한다.

                $("#smdis-grid").jqGrid({
                    caption: '코드 목록',
                    url:'/base/code/list?' + $("#searchForm").serialize(),
                    colNames:['id', '코드값', '코드명', '설명'],
                    colModel :[
                        {name:'id', index:'id', width:55, hidden:true},
                        {name:'code', index:'code', width:80},
                        {name:'name', index:'name', width:90},
                        {name:'descr', index:'descr', width:90},
                    ],
                    autoencode:true
                });

그리드 구현은 이걸로 끝이다. 
다음은 /base/code/list URL을 처리한 핸들러를 구현한다.

    @RequestMapping
    public void list(Model model, CodeSearchParam searchParam, PageParam pageParam) {
        model.addAttribute("codeList", codeService.list(pageParam, searchParam));
    }

화면에서 넘어온 파라미터들 중에 CodeSearchParam과 PageParam을 구분해서 받는다. @ModelAttribute가 적용된것이며 스프링 바인딩 기능이 적용되어 요청에 들어있는 파라미터들이 저 객체들의 속성으로 들어오게 된다.

처음엔 잘 넘어오는지 궁금해서 sout을 사용해서 콘솔에 찍어보면서 확인을 했었다. 특히 잘 넘어오더라도 한글이 잘 찍히는지 확인했다. 한글이 ??.?? 이렇게 넘어왔고, 파이어버그로 봤을 때는 화면에서 한글은 잘 넘어간 것 같다. 

톰캣 server.xml을 설정해줘야겠다.

    <Connector port="8080" protocol="HTTP/1.1" 
               connectionTimeout="20000" 
               redirectPort="8443" useBodyEncodingForURI="true" URIEncoding="UTF-8" />

이 뒤로는 한글이 잘 넘어왔다.

public class CodeSearchParam {

    private String name;

    private String code;

}

검색 옵션이고, 도메인 코드에 따라 검색 매개변수가 달랄 질 수 있으니 매번 만들어줘야겠다.

public class PageParam {

    //화면으로 넘여줄 값.
    int totalPageSize; // 전체 페이지 갯수
    int listSize; // 전체 목록 갯수
    int currentPageNumber; // 현재 보여줄 페이지

    //화면에서 넘어오는 값
    int page; //요청 받은 페이지
    int rows; //한 화면당 보여줄 줄 수
    String sidx; //정렬 기준 컬럼
    String sord; //정렬 방향

}

이건 여러 컨트롤러에서 공통으로 사용할 수 있는 코드다. 따라서 두 클래스 패키지를 잘 분리해둔다. CodeSearchParam은 base/code/support에 넣고 PageParam은 common/page 에 뒀다. PageParam에 주석으로 속성들을 분리해 뒀지만 사실 별도의 클래스로 나눌까 생각도 해봤다. 그런데 좀 귀찮았다. 어차피 서로 관련있는 정보들이기 때문에 그냥 한 곳에 뒀다.

다음은 서비스 인터페이스에 필요한 걸 정의하고 서비스 구현 클래스에서 메서드를 구현한다.

    public List<Code> list(PageParam pageParam, CodeSearchParam searchParam) {
        pageParam.initCurrentPageInfosWith(codeDao.totalSize(searchParam));
        return codeDao.list(pageParam, searchParam);
    }

이전에 만들었던 dao 코드를 써먹을 수 있게됐다. 그런데 변경해야겠다. 전체 사이즈를 구할때는 searchParam만 있으면 되고 실제 list를 가져올 땐 둘 다 넘겨줘야된다.

    public int totalSize(CodeSearchParam searchParam) {
        Criteria c = getCriteriaOf(Code.class);
        applySearchParam(c, searchParam);
        return (Integer)c.setProjection(Projections.count("id"))
            .uniqueResult();
    }

    public List<Code> list(PageParam pageParam, CodeSearchParam searchParam) {
        Criteria c = getCriteriaOf(Code.class);
        applySearchParam(c, searchParam);
        if(pageParam != null){
            c.setFirstResult(pageParam.getFirstRowNumber());
            c.setMaxResults(pageParam.getRows());
        }
        return c.list();
    }

    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        if(!searchParam.getCode().isEmpty()){
            c.add(Restrictions.ilike("code", searchParam.getCode(), MatchMode.ANYWHERE));
        }
        if(!searchParam.getName().isEmpty()){
            c.add(Restrictions.ilike("name", searchParam.getName(), MatchMode.ANYWHERE));
        }
    }

Criteria API를 사용해서 검색 옵션과 페이징 처리를 했다. PageParam 클래스에는

    public int getFirstRowNumber() {
        return Math.max((getPage() - 1) * getRows() ,0);
    }

    public void initCurrentPageInfosWith(int totalListSize) {
        setListSize(totalListSize);
        setTotalPageSize((int)Math.ceil((double)totalListSize /getRows()));
        setCurrentPageNumber(getPage());
    }

이런 코드가 추가됐다. 이제 테스트좀 해볼까. 인텔리J에 단축키를 Ctrl+J로 등록해놨다. 이클립스에서는 별도로 플러그인을 설치해야 이런 기능을 사용할 수 있는데.. 귀찮지 아니한가?

public class PageParamTest {

    @Test
    public void testGetFirstRowNumber() throws Exception {
        PageParam pageParam = new PageParam();
        pageParam.setRows(20);

        pageParam.setPage(0);
        assertThat(pageParam.getFirstRowNumber(), is(0));

        pageParam.setPage(-2);
        assertThat(pageParam.getFirstRowNumber(), is(0));

        pageParam.setPage(1);
        assertThat(pageParam.getFirstRowNumber(), is(0));

        pageParam.setPage(3);
        assertThat(pageParam.getFirstRowNumber(), is(40));
    }

    @Test
    public void testInitCurrentPageInfosWith() throws Exception {
        PageParam pageParam = new PageParam();
        pageParam.setRows(20);

        pageParam.initCurrentPageInfosWith(20);
        assertThat(pageParam.getListSize(), is(20));
        assertThat(pageParam.getTotalPageSize(), is(1));

        pageParam.initCurrentPageInfosWith(41);
        assertThat(pageParam.getListSize(), is(41));
        assertThat(pageParam.getTotalPageSize(), is(3));
    }
}

CodeDao도 테스트 해주지 않으면 왠지 섭섭하다. Criteria API를 잔뜩 썼는데 제대로 쓴 건지 확인차 학습차 확인 해보자.

 
   /**
     * testData2.xml
     *
     * <dataset>
            <code id="1" name="블랙" code="BLK" />
            <code id="2" name="레드" code="RED" />
            <code id="3" name="그린" code="GRN" />
            <code id="4" name="블루" code="BLU" />
            <code id="5" name="옐로" code="YLW" />
            <code id="6" name="골드" code="GLD" />
            <code id="7" name="실버" code="SLV" />
        </dataset>
     */
    @Test
    public void testToTalSize() throws Exception {
        insertXmlData("testData2.xml");

        CodeSearchParam codeSearchParam = new CodeSearchParam();
        codeSearchParam.setCode("L");
        assertThat(codeDao.totalSize(codeSearchParam), is(5));
    }

앗 이런 돌려보니 NullPointerException이다. 

        if(!searchParam.getName().isEmpty()){
            c.add(Restrictions.ilike("name", searchParam.getName(), MatchMode.ANYWHERE));
        }

여기서 발생했다. 아무래도 null 체크까지 추가해야겠다. @_@

    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        if(searchParam.getCode() != null && !searchParam.getCode().isEmpty()){
            c.add(Restrictions.ilike("code", searchParam.getCode(), MatchMode.ANYWHERE));
        }
        if(searchParam.getName() != null && !searchParam.getName().isEmpty()){
            c.add(Restrictions.ilike("name", searchParam.getName(), MatchMode.ANYWHERE));
        }
    }

코드 참.. 거시기 하다. @_@ 그래도 일단 테스트부터 성공시키자. 오퀘 테스트가 잘 돌았다. 리팩토링 하자.

public class CriteriaUtils {
    public static void addOptionalLike(Criteria c, String fieldName, String value) {
        if(value != null && !value.isEmpty()){
            c.add(Restrictions.ilike(fieldName, value, MatchMode.ANYWHERE));
        }    
    }
}

이런 유틸 하나를 만들었다.

    private void applySearchParam(Criteria c, CodeSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "code", searchParam.getCode());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
    }

DAO 코딩이 한결 간편해졌다. 테스트 해보자, 잘 돌아간다. 테스트를 보강하자.

    @Test
    public void testToTalSize() throws Exception {
        insertXmlData("testData2.xml");

        CodeSearchParam codeSearchParam = new CodeSearchParam();

        //code 겁색
        codeSearchParam.setCode("L");
        assertThat(codeDao.totalSize(codeSearchParam), is(5));

        //대소문자 구분 안함.
        codeSearchParam.setCode("l");
        assertThat(codeDao.totalSize(codeSearchParam), is(5));

        //name 검색추가
        codeSearchParam.setName("블");
        assertThat(codeDao.totalSize(codeSearchParam), is(2));
    }

잘 돈다. 이제 사이즈 구하는 쿼리는 안심이다.

    @Test
    public void testList() throws Exception {
        insertXmlData("testData2.xml");

        CodeSearchParam codeSearchParam = new CodeSearchParam();
        PageParam pageParam = new PageParam();
        pageParam.setRows(5); // 한 페이지에 5개씩 보자.
        pageParam.setPage(1); // 첫페이지.

        List<Code> codeList = codeDao.list(pageParam, codeSearchParam);
        assertThat(codeList.size(), is(5));
        System.out.println(codeList);

        pageParam.setPage(2);
        codeList = codeDao.list(pageParam, codeSearchParam);
        assertThat(codeList.size(), is(2));
    }

페이지 처리 테스트 코드인데 사실 좀 부실하다;;  그래도 이정도 해놓고 화면에서 확인해보니 잘 나온다. 오퀘.












top


[회사일] 그리드 출력하기 with JqGrid

프로젝트/SLT : 2010.06.03 10:09


다음으로 한 일은 그리드다. 회사에서 엑티브 위젯인가 뭔가 하는 제품 라이선스를 샀지만 난 그냥 제이쿼리 grid를 쓰고 싶어졌다. 그래서 찾아봤더니 JqGrid가 아주 쓸만해 보였다. 애초에 서버쪽이랑 연동 되도록 만들어져있었다. 가끔 잡습 그리드들을 보면 서버랑은 전혀 별개로 페이지 기능을 제공하는 것들이 있는데... 대체 그게 무슨 소용인가 싶다. 화면에 다 보여줄 것도 아니면서 뭐하러 미리 100개를 화면으로 가져온다음에 그걸 또 페이징 해서 보여줄까? 차라리 100개를 다 보여주고 말지.. 그래서 사실상 클라이언트단 페이징은 거의 쓸모가 없고 클아이언트용 페이지는 무조건 한페이지고 서버단과 연동하여 페이징할 네비게이션 바를 별도로 구현해야하는 피곤함이 뒤따른다. 그런데 JqGrid는 그러지 않았다. 서버단과 연동하여 페이징을 하기 때문에 네비게이션바도 별도로 구현할 필요가 없었고, 그리드 상에서 편집, 트리형 그리드 등 고급 기능도 잘 지원해주고 있었다.

http://www.trirand.com/blog/

역시 예제 먼저 확인했다.

http://trirand.com/blog/jqgrid/jqgrid.html

맘에 든다. 적용하려는데 레이아웃 플러그인처럼 간단하진 않았다.

1. jquery-ui theme와 밀접한 관계가 있는 플러그인이라 테마 롤러를 사용해서 원하는 테마를 설치했다.


2. json 배열을 어떻게 다루는지 자세히 알아봤다.

 jsonReader 옵셥을 사용하면 스프링 3.0 컨텐츠 네고 기능을 사용하기 좋았다. 그렇지 않고 @ResponseBody를 사용해서 JSON만 뿌려주게 하면 인코딩 문제도 생기도 JSON 전용 핸들러가 되버리니까 좀 별로였다. 그냥Model을 사용해서 화면에 전달할 객체를 넣어주고 json 형태를 원한다고 요청하면 컨텐츠 네고를 타고서 결국 Model에 들어있는 객체들을 json으로 변환해준다. 만약 브라우저 주소창에 Ajax로 보낸 요청과 같은 URL을 입력했다면 난 .jsp 페이지를 볼 수 있었을 것이다. 그런게 컨텐츠 네고 뷰 리졸버다. 암튼 지간에.. 여기서 인코딩과 JSON 다루는 방법을 학습하느라 시간좀 뺏겼다. 그래도 결국 해냈다.


여기서 다듬기 작업 중에 몇가지 못한걸 적어둔다.
- 레이아웃 크기 변경시 그리드 사이즈 자동 조정: 레이아웃 콜백 옵션이 잘 동작하지 않았다. 버그 같은데; 버전 업을 기다려봐야겠다.
- 그리드 세로 크기도 가로 크기처럼 자동으로 꽉 채워주는 옵션이 있으면 좋겠는데 없었다. 현재는 그리드 내용에 따라서 세로 크기가 자동으로 변하게 해뒀는데 머.. 이것도 나쁘지 않았다.

top


[회사일] 화면 레이아웃 잡기

프로젝트/SLT : 2010.06.03 09:59


난 화면 코드는 잘 못만든다. 만들긴 만드는데 시간이 너무 오래 걸린다. 그 시간에 자바 코딩을 하는편이 더 효율적이다. 하지만 회사에 프론트 엔드 개발자는 없다. 내가 다 해야된다. 그래서 그 동안 봄싹 개발을 하며 자바스크립트랑 도 좀 친해지고, CSS, HTML 과도 친해지려고 노력했다.

차마 ext-js까진 못했지만 jquery까지는 이제 플러그인 가져다 쓰고 옵션 주면서 쓰는데는 익숙해졌다. 화면 구상을 위해서 제일 먼저 검새해본게 jquery layout이고 젤 상위에 걸려있는 링크가 다음과 같다.

http://layout.jquery-dev.net/

언제나처럼 일단 데모 페이지로 가서 어떻게 동작하는지 살펴본다.

http://layout.jquery-dev.net/demos.cfm

일단 디자인 깔끔하고 레이아웃도 다양하게 만들 수 있어 보인다. 괜히 제일 위에 링크되어 있는게 아니었다.

http://layout.jquery-dev.net/demos/accordion.html

맘에 드는 예제를 찾았다.

상단은 제목과 로그인 정보
왼쪽은 메뉴
오른쪽은 검색 옵션
하단은 카피라이트 정도?
가운데는 그리드를 띄우고
추가/수정 화면은 별도의 창에 띄우기로 결정했다.


텍스트를 채워넣고 CSS로 스타일을 조금 넣었다.
top


[헤드 퍼스트 아이폰] 실습 완료!!

IPhone : 2010.06.02 11:46



휴... 드디어 끝났네..
아이폰 개발 입문서로는 최고인것 같아요.
역시 헤드 퍼스트 책은 멋져... +_+

이제 마포 갔다가 오면서 투표하고 빨래하고 강의 준비해야겠군. 
바쁘네 바빠;
top


[회사일] DAO 테스트 만들기

프로젝트/SLT : 2010.06.01 18:55


이전 코드에서는 뭐 테스트 해보고 싶을 만한게 없었다. 사실 하두 자주 써먹었고 거의 API를 그대로 쓴 코드라서 테스트 하지 않아도 확신이 서는 코드가 대부분이었다. 그 중에서도 굳이 가장 불안한 코드를 꼽으라면 DAO 코드가 되겠다. 나머진 그냥 단순 위임이라 뭐 의심할께 없다.

DAO 코드를 보자.

    public List<Code> list() {
        return getCriteriaOf(Code.class).list();
    }

흠.. 테스트 할 맛이 안난다.

   private Session getSession() {
        return sessionFactory.getCurrentSession();
    }

    private Criteria getCriteriaOf(Class clazz){
        return getSession().createCriteria(clazz);
    }

이런 private 메서드를 쓰고 있지만 전혀 테스트 할만한 뭐시기가 감지되지 않는다.

list 사이즈를 구하는 메서드를 만들어 보기로 했다. 페이징을 구현할텐데 거기서 분명히 전체 목록 크기를 구하는 쿼리가 필요할 것이기 떄문이다.

CodeDao 인터페이스에 코드를 추가하자.

    int totalSize();

다음은 이것을 구현한다.

    public int totalSize() {
        return (Integer)getCriteriaOf(Code.class)
            .setProjection(Projections.count("id"))
            .uniqueResult();
    }

오.. 제법 낯선 코드를 두 중 정도 코딩했다. 왠지 쬐끔 불안하다. 쬐금.. API 학습 차원에서 테스트를 해봐야겠단 생각이 든다. 좋아 결심했어.

소스 코드와 동일한 패키지에 CodeDaoImpleTest라는 테스트를 만든다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/testContext.xml")
@Transactional
public class CodeDaoImplTest extends DBUnitSupport {

    @Autowired CodeDao codeDao;

    @Test
    public void testTotalSize() throws Exception {
        insertXmlData("testData.xml");
        assertThat(codeDao.totalSize(), is(1));
    }

}

여기서 주목할 것은 굵게 표시한 부분이다. 먼저 테스트에서 사용할 DB가 운영 DB와 같으면 테스트 데이터를 넣고 확인할 때 문제가 될 수 있으니 전혀 다른 DB 설정을 보도록 테스트용 애플리케이션 컨텍스트와 데이터베이스 프로퍼티 파일을 만든다.

    <context:property-placeholder location="classpath*:test.database.properties"/>

이 부분 빼곤 나머진 같다.

database.password=
database.username=sa
database.url=jdbc:hsqldb:mem:ㅇㅇㅇ
database.driverClassName=org.hsqldb.jdbcDriver
hibernate.dialect=org.hibernate.dialect.HSQLDialect

다음은 DBUnit을 사용하기 쉽게 해주는 DBUnitSupport 클래스다. DataSource를 필요로 하기 때문에 자동 주입 받도록 설정한다.

public class DBUnitSupport {

enum DataType {EXCEL, FLATXML}

@Autowired
private DataSource dataSource;

protected void cleanInsertXmlData(String fileSource) throws Exception {
insertData(fileSource, DataType.FLATXML, DatabaseOperation.CLEAN_INSERT);
}
protected void cleanInsertXlsData(String fileSource) throws Exception {
insertData(fileSource, DataType.EXCEL, DatabaseOperation.CLEAN_INSERT);
}

private void insertData(String fileSource, DataType type, DatabaseOperation operation) throws Exception {
InputStream sourceStream = new ClassPathResource(fileSource, getClass()).getInputStream();

IDataSet dataset = null;
if (type == DataType.EXCEL) {
dataset = new XlsDataSet(sourceStream);
}
else if (type == DataType.FLATXML) {
dataset = new FlatXmlDataSet(sourceStream);
}

operation.execute(
new DatabaseConnection(DataSourceUtils.getConnection(dataSource)), dataset);
}

protected void insertXmlData(String fileSource) throws Exception {
insertData(fileSource, DataType.FLATXML, DatabaseOperation.INSERT);
}

protected void insertXlsData(String fileSource) throws Exception {
insertData(fileSource, DataType.EXCEL, DatabaseOperation.INSERT);
}

}

DataSource가 필요하기 때문에 
나머지 코드에 대한 설명은 생략~

<dataset>
<code id="1" name="블랙" code="BLK" />
</dataset>

테스트는 성공한다.

자 이제 DaoTest라는 클래스를 만들어서 설정을 옮겨보자.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("/testContext.xml")
@Transactional
public class DaoTest extends DBUnitSupport {
}

참고로 이 클래스를 src 밑에서 만들었다면 컴파일 에러가 난다.

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>

여기서 스코프를 test라고 했기 때문이다. 지워준다. 프레임워크성 코드가 생기기 시작하기 때문에 패키징을 조금 신경써서 나눠둔다.

public class CodeDaoImplTest extends DaoTest {

    @Autowired CodeDao codeDao;

    @Test
    public void testTotalSize() throws Exception {
        insertXmlData("testData.xml");
        assertThat(codeDao.totalSize(), is(1));
    }
}

자. CodeDaoImplTest가 깔끔해졌다. 앞으로 DAO 테스트를 할 땐 이렇게 DaoTest만 확장해서 만들면 되겠다.






top


[회사일] 초간단 계층형 아키텍처 만들기

프로젝트/SLT : 2010.06.01 18:30


Code 라는 도메인이 있다. Item에서 사용할 색, 사이즈, 제품 성별 등을 이 '코드'라는 걸로 관리할 생각인데 그걸 대편하는 도메인이 Code다.

1. 도메인 클래스 만들기

@Entity
public class Code {

    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private int id;

    @Column(length = 50)
    private String name;

    @Column(length = 50)
    private String code;

    @Column
    @Type(type = "text")
    private String descr;
...
}

2. JPA 애노테이션을 사용해서 매핑 정보를 입력했다. CodeDao 인터페이스를 만든다.

public interface CodeDao {
    List<Code> list();
}

3. 구현체를 만든다.

@Repository
public class CodeDaoImpl implements CodeDao {

    @Autowired SessionFactory sessionFactory;

    public List<Code> list() {
        return getCriteriaOf(Code.class).list();
    }
}

4. CodeService 인터페이스를 만든다.

public interface CodeService {
    List<Code> list();
}

5. 구현체를 만든다.

@Service
@Transactional
public class CodeServiceImpl implements CodeService{

    @Autowired CodeDao codeDao;

    public List<Code> list() {
        return codeDao.list();
    }
}

6. CodeController를 만든다.

@Controller
@RequestMapping("/base/code/*")
public class CodeController {

    @Autowired CodeService codeService;

    @RequestMapping
    public void list() {

    }
}

7. web/WEB-INF/views/base/code/list.jsp 파일을 만든다.

생략

그리고 화면에서 확인한다.

아주 지겹게 써먹고 있는 아키텍처인데 사실 아직도 잘 못하고 있다. 
코드 제위치 잡아 주는게 쉬운 일이 아니다.
이번엔 잘 해보자.



top


[회사일] 프로젝트 세팅

프로젝트/SLT : 2010.06.01 18:20


본격적인 개발을 시작하기 전에 할일이 너무 많다. 그래서 처음엔 스프링 Roo로 개발할까 생각도 했다. 10분만에 내가 원하는 모든 세팅을 가지고 CRUD 애플리케이션을 돌릴 수 있었다. 하지만 디자인도 맘에 안들고 스캐폴딩 코드를 빼면 내가 뭘 어떻게 해야할지 감이 잡히지 않았다. 책이라도 있었으면 보면서 했을텐데 책은 아직 없고 레퍼런스도 빈약했다. 포기했다. 그냥 맨땅에서 다시 다 만들기로 마음 먹었다. 처음부터 그렇게 생각했으면 조금 더 빨리 시작할 수 있었을 것이다.

프로젝트 세팅 작업은 STS에서 했다. 아직 인텔리J에 완전히 익숙하지 못해서 그런지 초기 프로젝트 세팅은 왠지 이클립스가 편하다.

1. 다이나믹 웹 프로젝트 만들기

중간에 웹 컨텐츠 폴더를 web으로 바꿔줬다.

2. pom.xml 만들기

STS에는 m2c 플러긴이 기본으로 깔려있다.
프로젝트를 우클릭하고 pom.xml 파일 생성을 했다.
이제부터 메이븐과의 싸움이다.
(이전에는 뭐가 뭔 설명인지 몰라서 괜히 어려워 보였었는데 이제는 머 그냥 귀찮을 따름이다.)

3. 프로젝트 구조 설정하기.

내가 원하는 프로젝트 구조는 간단하다. 일반적인 자바 프로젝트 같이 보이는 메이븐 프로젝트를 원한다. 즉 프로젝트 최상위에는 src, test, web만 있으며 src와 test 밑에는 바로 패키지가 시작된다. 일반적인 메이븐 프로젝트 구조가 아니다. 난 이게 더 편하다. 메이븐 플젝 구조라면 소스 패키지 시작하기 전에 3중 폴더를 지나야한다. 리소스와 자바 파일도 구분되어 있다. 그림 그리기는 귀찮아서 패스.

<build>
<sourceDirectory>
${project.basedir}/src
</sourceDirectory>
<testSourceDirectory>
${project.basedir}/test
</testSourceDirectory>
        <resources>
            <resource>
                <directory>${project.basedir}/src</directory>
                <excludes>
                    <exclude>**/*.java</exclude>
                </excludes>
            </resource>
        </resources>
        <testResources>
            <testResource>
                <directory>${project.basedir}/test</directory>
                <excludes>
                    <exclude>**/*.java</exclude>
                </excludes>
            </testResource>
        </testResources>
</build>

 이렇게 해서 src에 메이븐 자바 소스 파일과 리소스 파일을 넣을 거구 test에 메이븐 자바 테스트 파일과 리소스 파일을 넣겠다고 설정해줬다.

4. 메이븐 플러그인 설정

설명 생략. 역시 귀찮음;;

4-1. maven-war-plugin 설정

            <plugin>
                <artifactId>maven-war-plugin</artifactId>
                <configuration>
                    <warSourceDirectory>web</warSourceDirectory>
                </configuration>
            </plugin>

4-2. maven-compiler-plugin 설정

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.6</source>
                    <target>1.6</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

4-3. maven-resources-plugin 설정

            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-resources-plugin</artifactId>
                <configuration>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>

4-4. maven-eclipse-plugin 설정

            <plugin>
                <artifactId>maven-eclipse-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <additionalProjectnatures>
                        <projectnature>org.springframework.ide.eclipse.core.springnature</projectnature>
                    </additionalProjectnatures>
                    <additionalBuildcommands>
                        <buildcommand>org.springframework.ide.eclipse.core.springbuilder</buildcommand>
                    </additionalBuildcommands>
                    <downloadSources>${downSource}</downloadSources>
                    <downloadJavadocs>${downJavadoc}</downloadJavadocs>
                    <wtpversion>1.5</wtpversion>
                    <wtpContextName>${wtpContextName}</wtpContextName>
                </configuration>
            </plugin>

4-5. maven-clean-plugin 설정

            <plugin>
                <artifactId>maven-clean-plugin</artifactId>
                <version>2.4</version>
                <configuration>
                    <filesets>
                        <fileset>
                            <directory>web/WEB-INF/lib</directory>
                        </fileset>
                        <fileset>
                            <directory>web/WEB-INF/classes</directory>
                        </fileset>
                    </filesets>
                </configuration>
            </plugin>

4-6. 플러그인 저장소 설정

    <pluginRepositories>
        <pluginRepository>
            <id>codehaus snapshot repository</id>
            <url>http://snapshots.repository.codehaus.org/</url>
            <releases>
                <enabled>true</enabled>
            </releases>
        </pluginRepository>
    </pluginRepositories>
 
5. 프로젝트 의존성 설정 

    <properties>
        <spring.version>3.0.2.RELEASE</spring.version>
    </properties>

    <dependencies>
        <!-- Spring depedencies -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.core</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.instrument</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.test</artifactId>
            <version>${spring.version}</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.web</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.web.servlet</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.jdbc</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.context</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.context.support</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.aop</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.aspects</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.orm</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>org.springframework.transaction</artifactId>
            <version>${spring.version}</version>
        </dependency>
        <!-- Hibernate dependecies -->
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>com.springsource.org.hibernate</artifactId>
            <version>3.3.2.GA</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>com.springsource.org.hibernate.annotations</artifactId>
            <version>3.4.0.GA</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>com.springsource.org.hibernate.annotations.common</artifactId>
            <version>3.3.0.ga</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>com.springsource.org.hibernate.ejb</artifactId>
            <version>3.4.0.GA</version>
        </dependency>
        <!-- Caching dependecies -->
        <dependency>
            <groupId>net.sourceforge.ehcache</groupId>
            <artifactId>com.springsource.net.sf.ehcache</artifactId>
            <version>1.6.2</version>
        </dependency>
        <!-- DBMS dependecies -->
        <dependency>
            <groupId>javax.persistence</groupId>
            <artifactId>com.springsource.javax.persistence</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>javax.transaction</groupId>
            <artifactId>com.springsource.javax.transaction</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>c3p0</groupId>
            <artifactId>c3p0</artifactId>
            <version>0.9.1.2</version>
        </dependency>
        <dependency>
            <groupId>postgresql</groupId>
            <artifactId>postgresql</artifactId>
            <version>8.2-507.jdbc3</version>
        </dependency>
        <!-- Logging depedencies -->
        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>jcl-over-slf4j</artifactId>
            <version>1.5.10</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>ch.qos.logback</groupId>
            <artifactId>com.springsource.ch.qos.logback.classic</artifactId>
            <version>0.9.9</version>
        </dependency>
        <!-- Test dependencies -->
        <dependency>
            <groupId>org.junit</groupId>
            <artifactId>com.springsource.org.junit</artifactId>
            <version>4.8.1</version>
        </dependency>
        <dependency>
            <groupId>org.hamcrest</groupId>
            <artifactId>com.springsource.org.hamcrest</artifactId>
            <version>1.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.mockito</groupId>
            <artifactId>com.springsource.org.mockito</artifactId>
            <version>1.8.4</version>
        </dependency>
        <dependency>
            <groupId>org.dbunit</groupId>
            <artifactId>com.springsource.org.dbunit</artifactId>
            <version>2.2.0</version>
        </dependency>
        <dependency>
            <groupId>org.hsqldb</groupId>
            <artifactId>com.springsource.org.hsqldb</artifactId>
            <version>1.8.0.9</version>
            <scope>test</scope>
        </dependency>
        <!-- Web dependencies -->
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>com.springsource.javax.servlet.jsp.jstl</artifactId>
            <version>1.2.0</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>com.springsource.javax.servlet</artifactId>
            <version>2.5.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>com.springsource.javax.servlet.jsp</artifactId>
            <version>2.1.0</version>
            <scope>provided</scope>
        </dependency>
        <dependency>
            <groupId>org.tuckey</groupId>
            <artifactId>com.springsource.org.tuckey.web.filters.urlrewrite</artifactId>
            <version>3.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>com.springsource.org.apache.commons.fileupload</artifactId>
            <version>1.2.1</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>com.springsource.org.codehaus.jackson</artifactId>
            <version>1.4.2</version>
        </dependency>
        <dependency>
            <groupId>org.codehaus.jackson</groupId>
            <artifactId>com.springsource.org.codehaus.jackson.mapper</artifactId>
            <version>1.4.2</version>
        </dependency>
        <!-- AspectJ dependencies -->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>com.springsource.org.aspectj.runtime</artifactId>
            <version>1.6.5.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>com.springsource.org.aspectj.weaver</artifactId>
            <version>1.6.5.RELEASE</version>
        </dependency>
    </dependencies>


직접 운영할 수 있거나 라이브러리 설치를 부탁할 수 있는 넥서스 같은 메이븐 저장소를 설정해주는게 좋은데 난 봄싹 저장소를 설정했다.

    <repositories>
        <repository>
            <id>springsprout nexus</id>
            <name>SpringSprout Nexus public</name>
            <url>http://dev.springsprout.org/nexus/content/groups/public</url>
        </repository>
        <repository>
            <id>springsprout snapshots</id>
            <name>SpringSprout Nexus public snapshots</name>
            <url>http://dev.springsprout.org/nexus/content/groups/public-snapshots/
            </url>
        </repository>
    </repositories>

6. web.xml 설정하기

이제 메이븐은 끝났다. 이제부턴 웹 프로젝트 설정과의 싸움이다.

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
id="WebApp_ID" version="2.5">

<display-name>ㅇㅇㅇ</display-name>

<context-param>
<param-name>webAppRootKey</param-name>
<param-value>ㅇㅇㅇ.root</param-value>
</context-param>

<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath*:applicationContext*.xml</param-value>
</context-param>

<filter>
<filter-name>CharacterEncodingFilter</filter-name>
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
<init-param>
<param-name>encoding</param-name>
<param-value>UTF-8</param-value>
</init-param>
<init-param>
<param-name>forceEncoding</param-name>
<param-value>true</param-value>
</init-param>
</filter>

<filter>
<filter-name>httpMethodFilter</filter-name>
<filter-class>org.springframework.web.filter.HiddenHttpMethodFilter</filter-class>
</filter>

<filter>
<filter-name>UrlRewriteFilter</filter-name>
<filter-class>org.tuckey.web.filters.urlrewrite.UrlRewriteFilter</filter-class>
</filter>

<filter>
<filter-name>Spring OpenSessionInViewFilter</filter-name>
<filter-class>org.springframework.orm.hibernate3.support.OpenSessionInViewFilter</filter-class>
</filter>

<filter-mapping>
<filter-name>Spring OpenSessionInViewFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>CharacterEncodingFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>httpMethodFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<filter-mapping>
<filter-name>UrlRewriteFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

<servlet>
<servlet-name>smdis2</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/webmvc-config.xml</param-value>
</init-param>
</servlet>

<servlet>
<servlet-name>Resource Servlet</servlet-name>
<servlet-class>org.springframework.js.resource.ResourceServlet</servlet-class>
</servlet>

<servlet-mapping>
<servlet-name>smdis2</servlet-name>
<url-pattern>/app/*</url-pattern>
</servlet-mapping>

<servlet-mapping>
<servlet-name>Resource Servlet</servlet-name>
<url-pattern>/resources/*</url-pattern>
</servlet-mapping>

<session-config>
<session-timeout>10</session-timeout>
</session-config>

<welcome-file-list>
<welcome-file>index.jsp</welcome-file>
</welcome-file-list>
<error-page>
<exception-type>java.lang.Exception</exception-type>
<location>/app/uncaughtException</location>
</error-page>

<error-page>
<error-code>404</error-code>
<location>/app/resourceNotFound</location>
</error-page>
</web-app>

스프링 Roo에서 만들어주는 web.xml을 배꼈다.
이 중에서 일부 필터는 사용할 ORM에 따라 바꿔야 하며, 스프링 웹 플로우도 필요하다. 아직 웹 플로우 의존성은 추가하지 않았는데 나중에 넣어도 상관없으니 그냥 쓴다.

7. URL Rewriter 설정

<?xml version="1.0" encoding="utf-8"?>
<!DOCTYPE urlrewrite PUBLIC "-//tuckey.org//DTD UrlRewrite 3.0//EN" "http://tuckey.org/res/dtds/urlrewrite3.0.dtd">

<urlrewrite default-match-type="wildcard">
<rule>
<from>/resources/**</from>
<to last="true">/resources/$1</to>
</rule>
<rule>
<from>/static/WEB-INF/**</from>
<set type="status">403</set>
<to last="true">/static/WEB-INF/$1</to>
</rule>
<rule>
<from>/static/**</from>
<to last="true">/$1</to>
</rule>
<rule>
<from>/</from>
<to last="true">/app/index</to>
</rule>
<rule>
<from>/app/**</from>
<to last="true">/app/$1</to>
</rule>
<rule>
<from>/**</from>
<to>/app/$1</to>
</rule>
<outbound-rule>
<from>/app/**</from>
<to>/$1</to>
</outbound-rule>
</urlrewrite>

web.xml과 같은 폴더에 넣어준다. 스프링 Roo를 배꼈다. 

8. 스프링 디스패처 서블릿 설정 파일 만들기

web.xml 설정에 보면 이 파일 위치가 명시되어 있는 걸 볼 수 있다. 스프링 Roo에서 사용하는 기본 위치다. 그냥 똑같이 쓰도록 하자. web.xml에서 위치를 명시하지 않으면 web.xml과 같은 위치에 디스패처 서블릿의 이름-servlet.xml 파일로 만들어 줘야한다.

<?xml version="1.0" encoding="UTF-8" standalone="no"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:p="http://www.springframework.org/schema/p"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd     http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd     http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">

<context:component-scan base-package="ㅇㅇㅇ"
use-default-filters="false">
<context:include-filter expression="org.springframework.stereotype.Controller"
type="annotation" />
</context:component-scan>

<mvc:annotation-driven />

<mvc:view-controller path="/login" />
<mvc:view-controller path="/index" />
<mvc:view-controller path="/sample" />
<mvc:view-controller path="/uncaughtException" />
<mvc:view-controller path="/resourceNotFound" />
<mvc:view-controller path="/dataAccessFailure" />

<bean
class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver"
p:defaultErrorView="uncaughtException">
<property name="exceptionMappings">
<props>
<prop key=".DataAccessException">dataAccessFailure</prop>
<prop key=".NoSuchRequestHandlingMethodException">resourceNotFound</prop>
<prop key=".TypeMismatchException">resourceNotFound</prop>
<prop key=".MissingServletRequestParameterException">resourceNotFound</prop>
</props>
</property>
</bean>
<bean
class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
<property name="mediaTypes">
<map>
<entry key="atom" value="application/atom+xml" />
<entry key="html" value="text/html" />
<entry key="json" value="application/json" />
</map>
</property>
<property name="viewResolvers">
<list>
<bean class="org.springframework.web.servlet.view.BeanNameViewResolver" />
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="prefix" value="/WEB-INF/views/" />
<property name="suffix" value=".jsp" />
</bean>
</list>
</property>
<property name="defaultViews">
<list>
<bean
class="org.springframework.web.servlet.view.json.MappingJacksonJsonView" />
</list>
</property>
</bean>

<bean
class="org.springframework.web.multipart.commons.CommonsMultipartResolver"
id="multipartResolver" />

</beans>

스프링 Roo에서 배낀 코드에 몇 가지 설정을 변경하고 컨텐츠 네고 설정을 추가했다. 컨텐츠 네고 설정은 토비님 책에 나오는 코드다. 머시냐면.. 웹 브라우저 주소창에 /code/list를 입력하면 JSTL 뷰를 보여주고 Ajax로 JSON 형태의 응답을 달라고 요청하면 JSON 뷰를 받을 수 있다. 정말 편하지 아니한가.. 

하지만 아마도 프로젝트 진행하는 도중 이 파일은 자주 손보게 될 것 같다. mvc:annotation에서 기본으로 등록해주는 핸들러 매핑이나 어댑터를 확장하려면 어쩔 수 없다.

9. applicationContext.xml 만들기

src 폴더에 만들어준다.

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:jee="http://www.springframework.org/schema/jee"
       xmlns:lang="http://www.springframework.org/schema/lang"
       xmlns:p="http://www.springframework.org/schema/p"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:util="http://www.springframework.org/schema/util"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee.xsd
http://www.springframework.org/schema/lang http://www.springframework.org/schema/lang/spring-lang.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd">

    <context:property-placeholder location="classpath*:*.properties"/>

    <context:component-scan base-package="smdis">
        <context:exclude-filter expression="org.springframework.stereotype.Controller" type="annotation"/>
    </context:component-scan>

    <bean class="com.mchange.v2.c3p0.ComboPooledDataSource" id="dataSource">
        <property name="driverClass" value="${database.driverClassName}"/>
        <property name="jdbcUrl" value="${database.url}"/>
        <property name="user" value="${database.username}"/>
        <property name="password" value="${database.password}"/>
    </bean>

    <tx:annotation-driven/>

    <!-- ============================================================= -->
    <!--  Hibernate                                                    -->
    <!-- ============================================================= -->
    <bean id="transactionManager"
          class="org.springframework.orm.hibernate3.HibernateTransactionManager"
          p:sessionFactory-ref="sessionFactory"/>

    <bean id="sessionFactory"
          class="org.springframework.orm.hibernate3.annotation.AnnotationSessionFactoryBean">
        <property name="dataSource" ref="dataSource"/>
        <property name="packagesToScan" value="smdis" />
        <property name="hibernateProperties">
            <props>
                <prop key="hibernate.dialect">${hibernate.dialect}</prop>
                <prop key="hibernate.show_sql">true</prop>
                <prop key="hibernate.hbm2ddl.auto">update</prop>
            </props>
        </property>
    </bean>

</beans>

네임스페이스 설정 빼면 별거 없다. 컴포넌트 스캔이 없던 시절은 생각하기도 싫다.

10. database.properties 파일을 만든다.

#Updated at Tue May 25 14:01:35 KST 2010
#Tue May 25 14:01:35 KST 2010
database.password=ㅇㅇㅇ
database.username=ㅇㅇㅇ
database.url=jdbc:postgresql://localhost/ㅇㅇㅇ
database.driverClassName=org.postgresql.Driver
hibernate.dialect=org.hibernate.dialect.PostgreSQLDialect

DB는 PostgereSQL을 사용하기로 결정했다.

11. index.jsp 만들기

대충 Hello 찍는 JSP 파일을 만들고 끝!
드디어 코딩을 시작할 수 있겠다.

top


[회사일] 간단한 재고 조사 시스템 개발

프로젝트/SLT : 2010.06.01 17:38



현재 재고조사는 엑셀로 하고 있다. 엑셀에 이미지들을 잔뜩 붙여놓고 쉬트마다 또 복사해서 이미지를 잔뜩 붙여서 현재 창고에 총 수량이 몇개 남았는지 매일 매일 기록하고 있다. (내가 하고 있는게 아니라 이곳 직원들이 하고 있다.) 엑셀 용량이 50M에 달한다고 한다. 엑셀 이미지를 쉬트 한곳에만 올리고 나머진 그 링크 타고 보여주도록 하면 용량좀 줄일 수 있을 것 같은데 그렇게 되나 안되나 모르겠다. 아마도 되겠지.. 그러나 아직은 손대고 싶지 않다. 나중에 기분 좋을 때 해줘야겠다.

대표님이 원하는건 매일 창고에서 나가고 들어온 수량을 확인하고 현재 창고에 어떤 제품이 몇개나 남아있는지 알고 싶다고 하신다. 매일 저 엑셀파일로 결제를 하지만 엑셀 파일로는 위와 같은 상황이기 때문에 하루치 데이터만 유지하기도 버거운 상태다. 판매량 추이나 재고의 변화를 보려면 위해서 시스템을 필요로 하신다.

하지만 현업에 종사하시는 분들의 업무도 역시 엑셀 파일 작업만 하기도 벅찬상태다. 여기서 이 시스템에 정보를 입력해달라는 요구를 하면 나한테 짜증 낼 판국이다. 내가 시킨일도 아닌데 나한테 불평을 토로해봤자... 그래도 난 친절한 개발자니까 일단은 지금 쓰고 있는 엑셀 파일만 업로드하면 거기서 정보를 친절하게 읽어다가 DB에 넣어주도록 만들어야겠다. 그 다음엔 차차 이 시스템에 재고 정보를 입력하는데 익숙해지기를 바래야겠다.

이 시스템에서 가장 중요한 도메인은 Item 과 Inventory가 될 것 같다. 
흠냐 거럼 어디 일을 시작해볼까..
top

TAG 회사일

[헤드 퍼스트 아이폰] 막힌 곳 돌파!

IPhone : 2010.05.31 00:29


그러나;; 왜 막혔었는지는 모르겠다. 짐작만 갈 뿐... 시뮬레이터에서 앱을 지웠다가 다시 실행해봤었어야 하는데 그러지 않아서 데이터 모델이나 엔티티를 새로 만들었어도 이전에 사용한 sqlite 정보들과 맞지 않아서 그런 현상이 나타났던것 같다고.. 짐작만 갈뿐이다. 이미 그 플젝을 지워버려서 확인할 길도 없다.. OTL..


좋아 이제 DB 사용하는 법도 알았으니 제법 앱 스러운걸 만들 수 있을 것 같다.
흠. 그런데 과연 내가 코어 데이터를 잘 쓸 수 있을까;; 내용이 좀 부실한 것 같은데 머 다른책을 뒤져보면 나오겠지~
top


[헤드 퍼스트 아이폰] 드디어 막혔다;;

IPhone : 2010.05.30 21:50


reason = "Can't find model for source store"


이 에러 때문에;; 막혔다.. @_@;; 엔티티에 해당하는 클래스를 지우고 다시 만들어도 소용이 없었다. 
데이터 모델을 지우고 다시 만들어도 소용이 없었다.
데이터 모델을 지우고, 엔티티도 지우고 다시 만들었지만 소용이 없었다.
앱델리게이트 소스에서 options 부분도 추가해보고, 주석처리도 해봤지만 소용이 없었다.
코어 데이터 때문에 막히다니..


아흑.. 답답하다. 처음부터 다시 만들어야겠다.
top




: 1 : 2 : 3 : 4 : 5 : 6 : ··· : 88 :