Whiteship's Note

'2010/06'에 해당되는 글 42건

  1. 2010.06.29 [Spring Security] http 네임스페이스 쓰려면 필터 이름은 항상 고정(?)
  2. 2010.06.25 전자정부 프레임워크 공통 컴포넌트 실행 성공(?) (2)
  3. 2010.06.23 OSAF 2.0 준비중 (4)
  4. 2010.06.23 [스프링 3.0 이해와 선택] 2차 교육 공지 (2)
  5. 2010.06.23 [회사일] GenericExcelView 만들기
  6. 2010.06.18 [회사일] JPA로 계층구조 매핑하기
  7. 2010.06.17 [하이버네이트 완벽 가이드] 드뎌.. 하이버 번역서가 나왔습니다. (16)
  8. 2010.06.15 테스트 주도 개발 : 고품질 쾌속개발을 위한 TDD 실천법과 도구 (2)
  9. 2010.06.15 [회사일] 다대다 관계 서브 그리드 CRUD 완성
  10. 2010.06.14 [회사일] 서브 그리드 뿌리기
  11. 2010.06.14 [회사일] Right 도메인 CRUD 구현 도메인 클래스부터 화면까지
  12. 2010.06.14 [회사일] DateRange 추가
  13. 2010.06.14 [회사일] view.jsp 태그파일 적용
  14. 2010.06.14 [회사일] new.jsp 태그파일 적용
  15. 2010.06.14 [회사일] mgt.jsp 파일에 태그 파일 적용
  16. 2010.06.11 [회사일] edit.jsp 화면에 태그 파일 적용하기
  17. 2010.06.11 [회사일] GenericController 만들기
  18. 2010.06.11 [회사일] Generic 타입 추론 유틸 만들기
  19. 2010.06.10 [Hot potatoes] 문제 만들기 프로그램 (4)
  20. 2010.06.10 [집안일] 바느질하는 기선이 (4)
  21. 2010.06.09 [회사일] GenericService 버그 수정하기 (3)
  22. 2010.06.09 [회사일] MemberService 만들기. GenericService 만들기 (6)
  23. 2010.06.09 [회사일] Member 추가. MemberDAO 구현. GenericDAO 구현
  24. 2010.06.09 [회사일] CriteriaUtils 테스트하기 (2)
  25. 2010.06.09 [회사일] 검색에 enum 필드 추가하기
  26. 2010.06.09 [JSP 에디터] 인텔리J는 정말 똑똑해.
  27. 2010.06.08 [회사일] Enum 추가, Formatter 적용
  28. 2010.06.08 [회사일] GenericPersistentEnumFormatter 만들기
  29. 2010.06.08 [회사일] CRUD 화면 디자인 수정
  30. 2010.06.07 봄싹 마니산 등정 성공 (4)

[Spring Security] http 네임스페이스 쓰려면 필터 이름은 항상 고정(?)

Spring Security/etc : 2010.06.29 17:02


http://static.springsource.org/spring-security/site/docs/3.0.x/reference/appendix-namespace.html#nsa-http-attributes

<filter>
<filter-name>securityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

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

자 이렇게 필터를 설정하고 

<http>
        <intercept-url pattern="/base/color/mgt" access="ROLE_USER" />
        <intercept-url pattern="/**" access="IS_AUTHENTICATED_ANONYMOUSLY" />
<form-login login-page="/login" />
<logout logout-success-url="/index" />
<remember-me />
</http>

    <beans:bean id="smdisUserDetailsService" class="smdis.common.security.SmdisUserDetailsService"/>

<authentication-manager alias="authenticationManager">
<authentication-provider user-service-ref="smdisUserDetailsService"/>
</authentication-manager>

<global-method-security secured-annotations="enabled"
jsr250-annotations="enabled" pre-post-annotations="enabled" />

이렇게 시큐리티 설정을 했다.

잘 돌아갈까?? 안 돌아간다.. 시큐리티 네임스페이스를 사용해서 <http>를 등록하면 FileChainProxy 빈 이름은 항상 springSecurityFilterChain이 된다. 그래서 필터 이름을 springSecurityFilterChain으로 설정해줘야 한다.

뭐.. DelegatingFilterProxy 필터 자체에 targetBeanName 속성을 사용해서 연결할 빈 이름을 설정할 수도 있지만 기본적으로 이 이름은 필터 이름을 따르게 된다. 필터 이름을 바꾸고 targetBeanName을 또 설정해 주느니 그냥 필터 이름을 springSecurityFileterChain으로 하는게 좋겠다.

<filter>
<filter-name>springSecurityFilterChain</filter-name>
<filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
</filter>

    <filter-mapping>
        <filter-name>springSecurityFilterChain</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
저작자 표시
신고
top


전자정부 프레임워크 공통 컴포넌트 실행 성공(?)

모하니?/Coding : 2010.06.25 15:13



이 화면 보기가 너무 힘들다;

OSAF 예제 애플리케이션 띄우는거 보다 100배는 올래거리는것 같다. 

그나마 이 화면에서 다음으로 전개도 안된다. 왜냐면 DB가 세팅되지 않았기 떄문에;;

전자 정부 사이트에서 받은 mysql용 sql 파일들을 돌리다 보면 계속 에러가 난다. 자주 보이는 건 주키가 너무 길다는 에러인데 이건 주키 컬럼 사이즈(보통 메서드 이름이 200으로 잡혀 있었다.)를 줄여가면서 지나갔다. 그런데 두번째 sql 파일의 에러는 도무지 모르겠다; MySQL을 설치한지 오래되서 새 버전으로 올린 담에 해봐야 하는건지.. 그냥 오라클로 해야하는건지... 아흑..

맞다. DB만 만들다고 돌아가는것도 아니다. globals.properties 파일을 C:\Documents and Settings\MyHome\egovProps 이 위치에 넣어줘야 하고 이걸 제대로 쓰려면 그 파일에 있는 각종 설정 내용을 다 손봐줘야 한다.

이거.. 아무래도 내가 너무 무모만 것에 손을 댄것 같다. 덕분에 전자 정부 프레임워크 구조가 어떤지 파악할 수 있었다.

개발 환경쪽에서 '구현도구'라고 되어있는 이클립스를 받아서 전자정부 웹 프로젝트를 만들어야지 필요한 pom.xml이 나오지만 사실상 이건 제대로 된 pom.xml파일이 아닌것 같다. 그래도 일단 이렇게 받는다.

http://www.egovframe.org/wiki/doku.php?id=egovframework:배포_패키지_구성안

여기에서 공통여부 Y로 체크되어 있는 것은 무엇을 클릭하던 똑같이 egovframework-common-1.0.0를 받게 된다. 저 안에 수많은 공통 기능이 들어가 있다. 즉 저것을 넣어서 앱을 실행하면 전자정부 프레임워크 도입을 완수한거나 마찬가지다. 거기에 부가적으로 게시판이나 통계등을 넣고 싶다면 해당 메뉴를 클릭해서 소스를 받은 다음 프로젝트에 추가해줘야 한다. 

이 "공통 컴포넌트"라고 하는 서비스들은 "실행 환경"을 기반으로 하고 있다. 실행 환경 코드는 다시 네가지로 나뉜다. 공통기반, 데이터처리, 연계통합, 화면처리.. 이 중에서 웹 관련 기능이 궁금하다면 "화면 처리" 코드를 받아서 보면 된다.

하지만 안타깝게도 이 "실행 환경"에서 받는 코드들은 pom.xml이 없다. 빌드가 되지 않는다. 아참;; 위에서 설명하려다 까먹었는데.. "개발 환경"에서 자동으로 만들어준 pom.xml은 버리고 "공통 컴포넌트" 중에서도 "공통"에 해당하는 egov~~-common-1.0.0을 받으면 들어있는 pom.xml이 있다. 그걸 쓰면 "공통 컴포넌트"에 필요한 의존성까지 설정되어 있으니 무난히 빌드할 수 있다. 어쨋거나 이 코드들은 그냥 쌩으로 봐야한다. 빌드가 안되니.. 별도의 jar 파일을 만들수도 없다. (원래 안만들어도 된다. jar 파일만 따로 다운 받을 수도 있고, 공통 컴포넌트의 pom.xml에서 의존성으로 알아서 가져오게 되어 있다.) 정... 하고 싶으면 pom.xml 만들고 필요한 의존성 다 붙여주면 되는데.. 글쎄;; 별로..

소스 중에 유일하게 살펴본 코드로는 "화면처리(ptl)"에 들어있는 SimpleUrlAnnotationHandlerMapping이 있는데 이름에서는 추측하기 어려운 일을 해준다. 핸들러 매핑 마다 일괄적용되는 인터셉터가 부담스러워서 그걸 url 리스트를 사용해서 걸러주는 작업이다. 그런데 토비님 책을 보면 스프링 3.0의 <mvc:inteceptor>로 해결이 가능하다. 글로벌한 인터셉터 설정이기 때문에 핸들러 매핑을 기준으로 생각할 필요가 없어졌다. 아마도 저 클래스는 주석에도 적혀있듯이 3.0 가면서 deprecated 될 것 같다.
저작자 표시
신고
top


OSAF 2.0 준비중

OSAF : 2010.06.23 17:05


최근에 계속 포스팅 했던 [회사일]을 하면서 다른 프로젝트에서도 사용할 수 있는 코드들을 별도 패키지에 모아뒀습니다. 

Generic 사용 극대화

이전과 마찬가지로 Generic 사용을 극대화해서 DAO, Service, Controller를 만들고 PersistentEnum을 활용하는 GenericPEFormatter도 만들어 뒀고, GenericExcelView까지 있습니다.

REST 스타일 URL 지원

스프링 3.0 최신 버전과 하이버네이트를 사용했으며 URL은 REST 스타일로 만들어 놨습니다. 따라서 GenericController만 상속받으면 기본적인 CRUD 기능을 RESTful URL로 제공해줍니다.

계층 구조

기본적인 계층 구조(DAO, Service, Controller) 형태기 때문에 적응하기 쉬울 겁니다. 스프링 Roo 스타일로 만들까 했지만. AsepctJ를 공부헀었는데도 잘 적응이 안되더라구요.ㅋ

no-iframe

참.. 화면이 이전처럼 iframe으로 나눠진게 아니라 통짜 구조입니다. 화면을 나누는건 JQuery layout 플러그인을 사용했구요.  iframe보다 편합니다.

그리드

그리드도 jqGrid를 사용해서 이전에 쓰던 그리드(뭔지도 기억안남)보다 기능도 많고 더 좋습니다.

상속

단점은.. Spring Roo 처럼 완전히 비침습적인 프레임워크가 아닌지라.. 상속을 사용해야 합니다. DAO, Service, Controller, Enum 들이 OSAF 코드를 상속 받아야 합니다.

코드 생성

코드 생성 기능은 없습니다. 만들라면 만들겠지만 귀찮기도 하고 손으로 직접 만들면서 사용법을 익히는게 좋을 것 같아서 만들지 않았습니다.

바로 시작

프로젝트를 다운받아서 라이브러리 형태로 넣을 필요없이 프로젝트를 받아서 바로 자신의 웹 프로젝트로 사용할 수 있게끔 배포할 계획입니다. 

기본 모델 제공

바로 실행할 수 있는 프로젝트에 Member, Right, Code, CodeCat 같은 기본 도메인을 제공합니다. 자주 쓰는 도메인 코드를 가진 상태에서 시작하세요. Generic류 코드를 어떻게 쓰는지, OSAF 태그파일을 사용해서 화면 코드를 작성하는 방법을 쉽게 볼 수 있습니다.
저작자 표시
신고
top

TAG OSAF 2.0

[스프링 3.0 이해와 선택] 2차 교육 공지

모하니?/Planning : 2010.06.23 14:10


http://www.hanbitedu.co.kr/incumbent/shortContent.do?index=1097

벌써 공지가 떴네요. 캬..
1차 교육 끝나자 마자 바로 시작합니다.
2차 강의 할 땐 토비님 책을 가지고 시작할 수 있겠군요.
한결 든든하겠네요.

- 1일차 (7월 24일) 
 1장 오브젝트와 의존관계 
 2장 테스트 
 3장 템플릿 
 4장 예외 

- 2일차 (8월 07일) 
 5장 서비스 추상화 
 6장 AOP 
 7장 스프링 핵심기술 응용 
 8장 스프링이란? 

- 3일차 (8월 14일) 
 9장 스프링 프로젝트 시작하기 
 10장 IoC 컨테이너와 DI 
 11장 데이터 엑세스 기술 

- 4일차 (8월 21일) 
 12장 스프링 웹 기술과 스프링 MVC 
 13장 스프링 @MVC 
 14장 AOP와 LTW 
 15장 콘텍스트 테스트 프레임워크 
 16장 스프링의 기타 기술과 효과적인 학습방법 

1일차와 2일차는 "이해"하는 단계입니다. 이 부분에서 스프링 IoC 컨테이너, 테스트, 템플릿, 트랜잭션 추상화, 이메일 추상화, 스프링 AOP 기능들이 어떻게 만들어졌는지 실습을 통해서 학습하게 됩니다.

3일차와 4일차는 "선택"하는 단계로 실제 웹 애플리케이션을 만들어 가면서 IoC 컨테이너 설정 방법, 연관 관계 설정 방법, 빈 등록 방법 등을 선택하고, DAO 기술을 선택하고 그에 따른 스프링 지원 기술을 사용하며, 트랜잭션 선언 방법도 선택하고 적용합니다. 4일차에서는 @MVC를 사용해서 웹 애플리케이션을 마무리하고 3.0에 추가된 기능들을 학습합니다.


저작자 표시
신고
top


[회사일] GenericExcelView 만들기

프로젝트/SLT : 2010.06.23 11:24


현재 업무를 엑셀 파일로 처리하고 있기 때문에 엑셀 파일과의 연동을 필수적입니다. 따라서 시스템에 들어있는 정보를 엑셀로 받아볼 수 있게 하거나 엑셀 정보를 시스템에 올려서 DB에 저장되게 하는 것도 필요합니다.

엑셀 업로드야 엑셀 파일 형식이 도메인 클래스 마다 다를 테고 비즈니스 로직마다 다를테니 일반화하기 힘들지만(뭐 어떤 규칙을 정하면 일반화를 못할 것도 없겠지만..) 엑셀 다운로드 경우에는 일반화하기 좋습니다. 리포트 형식의 엑셀을 원하는게 아니라면 말이죠;; 그런건 여기서 받은 엑셀 파일을 기초데이터로 삼아서 다른 직원보고 편집해서 리포트 만들라고 시키면 될테니 이런 간단한 재고관리 시스템에 무리한 기능을 넣고 싶진 않군요.

어쨋든.. 일반화 시켜봅시다.

<page:mgtpage label="재고 관리">
    <page:search label="재고 검색">
        <s:input label="제품명" path="name" />
        <s:input label="제품번호" path="number" />
        <s:input label="색상" path="colorName" />
        <s:select label="성별" path="sexValue" items="${ref.sexList}" />
        <s:input label="공급업체" path="suppName" />
        <s:input label="창고" path="locationName" />
        <s:date label="날짜" fromPath="dateRange.from" toPath="dateRange.to"/>
    </page:search>
    <script type="text/javascript">
        $(function() {
            $("#smdis-grid").jqGrid({
                colNames:['id', '제품명', '제품번호', '색상', '사이즈', '공급업체', '창고', '수량', '날짜'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'item.name', index:'item.name', width:90},
                    {name:'item.number', index:'item.number', width:90},
                    {name:'item.colorName', index:'item.color', width:90},
                    {name:'item.size', index:'item.size', width:90},
                    {name:'item.supp.name', index:'item.supp', width:80},
                    {name:'location.name', index:'location.name', width:80},
                    {name:'qty', index:'qty', width:80},
                    {name:'date', index:'date', width:80}
                ]
            });
        });
    </script>
</page:mgtpage>

화면에 보여줄 그리드 정보가 들어있는 mgt.jsp 페이지 소스입니다. 태그 파일을 사용해서 중복 코드를 제거하고 여기서 꼭 다뤄야하는 정보만 남겨뒀습니다. 이 상태에서 엑셀을 일반화 시켜서 만들때 서버쪽으로 넘겨줘야 하는 정보가 들어있습니다. 컬럼명과 각 컬럼 값이 들어있는 path 정보입니다.

화면에 등록된 그리드 정보를 그대로 엑셀로 옮기고 싶다면 그 두 정보를 가져와야 합니다. 구현 과정을 전부다 설명하기는 다소 귀찮군요.. 흠.. 할일도 많으니까 대충 생략하겠습니다.


저기서 버튼을 누르면 컬럼 정보, Path 정보, 검색 매개변수들을 서버쪽으로 넘겨줍니다.

그럼 서버가 받아서 SpEL과 JExcel API와 스프링이 제공해주는 AbstractJExcelView 엑셀뷰를 사용해서 엑셀을 만들어 줍니다.

public class GenericExcelView extends AbstractJExcelView {

    protected void buildExcelDocument(Map<String, Object> model, WritableWorkbook wb, HttpServletRequest req, HttpServletResponse res) throws Exception {
        List<?> list = (List<?>) model.get("list");
        String modelName = (String) model.get("modelName");
        List<String> colNameList = (List<String>) model.get("colNameList");
        List<String> colPathList = (List<String>) model.get("colPathList");

        res.setHeader("Content-Disposition", "attachment; filename=" + modelName + ".xls");
        WritableSheet sheet = wb.createSheet(modelName, 0);

        for(int column = 0 ; column < colNameList.size() ; column++){
            sheet.addCell(new Label(column, 0, colNameList.get(column)));
        }

        ExpressionParser parser = new SpelExpressionParser();
        int row = 1;
        for(Object object : list){
            StandardEvaluationContext context = new StandardEvaluationContext(object);
            for(int column = 0 ; column < colPathList.size() ; column++){
                String path = colPathList.get(column).replace(".", "?.");
                String value = parser.parseExpression(path).getValue(context, String.class);
                sheet.addCell(new Label(column, row, value));
            }
            row++;
        }
    }
}

이걸 사용하도록 GenericController 쪽에도 기능을 추가해줬는데 그거야 뭐 간단하니깐. 생략하겠습니다. 이제 재고관리와 상품관리쪽에 엑셀 업로드를 만들어야겠습니다. 내일할까나;; 흠.


저작자 표시
신고
top


[회사일] JPA로 계층구조 매핑하기

프로젝트/SLT : 2010.06.18 15:24


하이버 번역서를 보시면 여러가지 방법이 나와있는데 그 중에서 가장 간단하면서, 성능도 좋고, 다형성까지 살릴 수 있는 방법으로 "계층 구조 당 테이블" 매핑 방법을 소개하고 있습니다.

원래 이 회사에서 다루는 재고가 '신발' 하나였는데 이제 곧 '가방'까지 늘어날 예정인가 봅니다. 고객(울 회사 대표님)과 대화를 하지 않았다면 몰랐을텐데.. 초기에 알았으니 그나마 다행입니다. Item이라는 클래스에 신발 정보 다 넣어놓고 이런 얘기 했다면... @_@ 좀 피곤했을텐데 말이죠. 다행히 테스트용으로 두 세개 밖에 안넣어봤거든요.ㅋ

어쨋든..

Item을 상위클래스로 두고, Shoes와 Bag을 하위 클래스로 설계하고 싶어졌습니다.

그래서 모든 상품에 기본적으로 들어갈만한 속성은 Item에 남겨두고 JPA 계층 구조 매핑을 추가했습니다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(
    name = "ITEM_TYPE",
    discriminatorType = DiscriminatorType.STRING
)
public class Item {

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

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

    @Column(length = 50)
    private String name;

    @Column(length = 50)
    private String number;

    @Column
    private String image;

    @Column
@Type(type="smdis.domain.usertype.SexUserType")
    private Sex sex;

    @ManyToOne
    private Color color;
...
}

그다음 이 클래스를 상속받는 Shoes라는 도메인을 만들었습니다.

@Entity
@DiscriminatorValue("SHO")
public class Shoes extends Item {
    
    @Column(name = "SHO_SIZE")
@Type(type="smdis.domain.usertype.ShoesSizeUserType")
    private ShoesSize size;

    public ShoesSize getSize() {
        return size;
    }

    public void setSize(ShoesSize size) {
        this.size = size;
    }
}

간단하네요. Bag도 추가해봐야겠습니다.



저작자 표시
신고
top


[하이버네이트 완벽 가이드] 드뎌.. 하이버 번역서가 나왔습니다.



Java Persistence With HIbernate 번역서가 나왔습니다.
어흑.. 몇년째 번역한건지.. 원래 이 책을 프로 스프링 2.5보다 먼저 번역 하고 있었거든요.
사실 그보다 훨씬 더 전에는 물개선생님께서 번역하고 계셨었는데... ㅋ
하이버에 관심있으신 분들을 한권쯤...ㅋ



저작자 표시
신고
top


테스트 주도 개발 : 고품질 쾌속개발을 위한 TDD 실천법과 도구

모하니?/Thinking : 2010.06.15 16:19


http://www.hanb.co.kr/book/look.html?isbn=978-89-7914-726-1


드뎌 수원형 책이 나왔다. 캬~~
좋은 책들이 쏟아지려나보다.
좀있으면 토비님 책도 나오고 재성형 책도 나올텐데.. 

왠지 아는 사람들이 쓴 책을 접하게 되니까 기분이 좋다.
책에 적혀있는 텍스트를 읽으면서 저자분 성격에 비추어 어떠한 생각으로 어떤 기분으로 이 글을 썼는지 짐작하는 재미가 쏠쏠해서 일까...

암튼 왠지.. 난 이 분들을 만나봤다는 것 만으로도 참 기분이 좋다.
나도 언젠간...꼭!

그나저나 이책은 줄여서 뭐라고 불러야 하지...
'고품질 쾌속개발을 위한 TDD 실천법과 도구를 보세요.' 헥헥헥..
'빨간 TDD' 보세요 라고 할까나...
저작자 표시
신고
top


[회사일] 다대다 관계 서브 그리드 CRUD 완성

프로젝트/SLT : 2010.06.15 14:17



팝업은 팬시박스를 사용했습니다. 이게 저번에 선택했던 컬러 머시기 보다 더 깔끔하더군요.

http://fancybox.net/home

jqGrid 다중 선택 기능을 사용했는데 아주 깔끔합니다. 그리드도 잘 선택한것 같아요.

URL은 RESTful 하게 만든다고 만들어 봤는데..

    @RequestMapping(value="/{id}/rights", method = RequestMethod.GET)
    public void rights(@PathVariable Integer id, Model model, PageParam pageParam){
        model.addAttribute("list", service.rightListOfMember(id, pageParam));
    }

    @RequestMapping(value="/{id}/rights", method = RequestMethod.POST)
    public @ResponseBody String addRights(@PathVariable("id") Integer id, @RequestParam("ids[]") Integer[] ids){
        service.addRightsToMember(id, ids);
        return "success";
    }

    @RequestMapping(value="/{id}/rights", method = RequestMethod.DELETE)
    public @ResponseBody String deleteRights(@PathVariable("id") Integer id, @RequestParam("ids[]") Integer[] ids){
        service.deleteRightsFromMember(id, ids);
        return "success";
    }

잘 동작합니다. 히든 필드 이용해서 _method에 원하는 RequestMethod 실어서 보내주면 되니깐 아주 간단하네요. HTML5 부터는 이런일 안해도 될려나.. 근데 HTML5 지원하는 브라우저가 있어도 그걸 써야 말이죠;;

지금 상태에서 한가지 굉장히 맘이 불편한 코드가 있는데.. 바로 서비스 코드입니다.

    List<Right> rightListOfMember(Integer memberId, PageParam pageParam);

    void addRightsToMember(Integer memberId, Integer[] rightsIds);

    void deleteRightsFromMember(Integer memberId, Integer[] rightsIds);

별로 객체지향적으로 보이지 않아요.. Request Parameter Oriented 프로그래밍 같지 않나요.@PathVariable로 넘어오는 값에도 분명히 바인딩이 적용될텐데... 컨트롤러에서부터 Member 타입으로 바인딩해서 가져오면 저런 코드가..

    List<Right> rightListOfMember(Member member, PageParam pageParam);

    void addRightsToMember(Member member, Right[] rights);

    void deleteRightsFromMember(Member member, Right[] rights);

이렇게 될텐데 말이죠. 흠.. 뭐좀 먹고 와서 해봐야지.

저작자 표시
신고
top


[회사일] 서브 그리드 뿌리기

프로젝트/SLT : 2010.06.14 18:53



계정 정보 뷰 화면에 권한 정보를 뿌리도록 서브 그리드를 추가합니다.

화면 코드에서는 jqGrid를 사용해서 코드를 작성합니다.

    <p class="ui-widget-content">
        <h2 class="smdis-pane-title" id="smdis-grid-title">
            <ul class="smdis-left-icons">
                <li id="smdis-add-button" class="ui-state-default ui-corner-all" title="권한 추가"><span class="ui-icon ui-icon-plusthick"></span></li>
                <li id="smdis-delete-refresh" class="ui-state-default ui-corner-all" title="권한 삭제"><span class="ui-icon ui-icon-minusthick"></span></li>
            </ul>
            ${model.loginId} 권한 정보
        </h2>
        <table id="smdis-sub-grid"></table>
        <div id="smdis-sub-pager"></div>
    </p>
    <script type="text/javascript">
        $(function(){
            $("#smdis-sub-grid").jqGrid({
                colNames:['id', '이름', '설명'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'name', index:'name', width:100},
                    {name:'descr', index:'descr', width:100}
                ],
                pager: '#smdis-sub-pager',
                url:'${baseUrl}/${model.id}/rights'
            });
        });
    </script>

빨간 부분을 그리는데 이런 코드가 필요했습니다. 아직 버튼에 이벤트를 안달아서;; 좀 더 코드가 늘것 같네요. 일단은 서브 그리드를 그리는 기능만 만들겠습니다.

중요한 저 그리드에서 사용할 URL 핸들러를 만드는 거죠.

@Controller
@RequestMapping("/system/member")
public class MemberController extends GenericController<MemberService, MemberRef, Member, MemberSearchParam> {

    @RequestMapping("/{id}/rights")
    public void rights(@PathVariable int id, Model model, PageParam pageParam){
        model.addAttribute("list", service.rightListOfMember(id, pageParam));
    }

}

MemberController에 핸들러를 추가합니다. 평범해 보이지만 스프링 3.0 컨텐츠 네고 기능을 타고 JSON으로 바껴서 보내지게 됩니다. (물론 브라우저 주소창에 저 URL을 입력하면 JSP 페이지를 찾다가 원하는 페이지가 없다고 404 에러 코드가 보여지겠죠.)

public interface MemberService extends GenericService<Member, MemberSearchParam>{

    List<Right> rightListOfMember(int id, PageParam pageParam);
}

MemberService 인터페이스를 타고.. 

@Service
@Transactional
public class MemberServiceImpl extends GenericServiceImpl<MemberDao, Member, MemberSearchParam> implements MemberService {

    @Autowired RightService rightService;

    @Override
    public List<Right> rightListOfMember(int id, PageParam pageParam) {
        Member member = dao.getById(id);
        return rightService.listOfMember(member, pageParam);
    }
}

MemberServiceImpl에 와서.. id에 해당하는 Member를 꺼내주고 나머진 RightService에 위임합니다.

public interface RightService extends GenericService<Right, RightSearchParam>{
    List<Right> listOfMember(Member member, PageParam pageParam);
}

그럼 RightService 인터페이스를 타고..

@Service
@Transactional
public class RightServiceImpl extends GenericServiceImpl<RightDao, Right, RightSearchParam> implements RightService{

    public List<Right> listOfMember(Member member, PageParam pageParam) {
        pageParam.initCurrentPageInfosWith(member.getRights().size());
        return dao.listOf(member, pageParam);
    }
}

RightServcieImpl에서는 PageParam 값을 설정하고, DB에 다녀와야 할 일은 RightDao로 위임합니다.

public interface RightDao extends GenericDao<Right, RightSearchParam> {
    List<Right> listOf(Member member, PageParam pageParam);
}

그럼 RightDao 인터페이스를 타고..

@Repository
public class RightDaoImpl extends GenericDaoImpl<Right, RightSearchParam> implements RightDao {

    protected void applySearchParam(Criteria c, RightSearchParam rightSearchParam) {
        CriteriaUtils.addOptionalLike(c, "name", rightSearchParam.getName());
    }

    public List<Right> listOf(Member member, PageParam pageParam) {
        Criteria c = getCriteriaOf(Right.class);
        c.createAlias("members", "m");
        c.add(Restrictions.eq("m.id", member.getId()));

        applyPagingParam(c, pageParam);
        applyOrderingParam(c, pageParam);
        
        return c.list();
    }
}

여기서 member가 가지고 있는 권한 목록을 가져옵니다.

public class RightDaoImplTest extends SpringTest {

    @Autowired RightDao rightDao;

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

    @Test
    public void listOf() throws Exception {
        insertXmlData("testData.xml");

        Member member = new Member();
        member.setId(1);

        PageParam pageParam = new PageParam();
        pageParam.setRows(20);
        pageParam.setPage(0);

        List<Right> rights = rightDao.listOf(member, pageParam);
        assertThat(rights.size(), is(2));

        Right right = (Right)rights.get(0);
        assertThat(right.getId(), is(1));
        assertThat(right.getName(), is("admin"));
    }

}

<dataset>
<member id="1" loginid="whiteship" password="1" email="email@com" name="기선" />

    <rights id="1" name="admin"/>
    <rights id="2" name="user"/>

    <member_rights members_id="1" rights_id="1"/>
    <member_rights members_id="1" rights_id="2"/>
</dataset>

DAO 테스트에서는 원하는 Right를 가져오는지 확인합니다.

    @Test
    public void listOfMember() throws Exception {
        insertXmlData("testData.xml");
        Member member = memberDao.getById(1);

        PageParam pageParam = new PageParam();
        pageParam.setPage(0);
        pageParam.setRows(20);

        List<Right> rs = rightService.listOfMember(member, pageParam);

        assertThat(pageParam.getListSize(), is(2));
        assertThat(pageParam.getTotalPageSize(), is(1));
    }

서비스 테스트에서는 PageParam에 값이 잘 설정되는지 확인했습니다.

이제 잘 될 줄 알고 서버를 켜고 돌려봤더니.. 이런;;;


이런 문제가 생기더군요. 물론 그리드에도 제대로 표시가 되지 않았습니다.

Member <--> Right 양방향 관계로 설정했을 때 쌍방에서 가지고 있는 rights나 members가 비어있더라도 JSON 뷰로 넘어갔을 때 OSIV 필터에 의해서 무조건 값을 읽어오게 됩니다. 실제로 해당 레퍼런스가 아주 null이었으면 가져오려는 시도도 안할텐데 null도 아닙니다. Member나 Right에서 초기화 해준적도 없는데도 null이 아닙니다. 그건 하이버네이트 Lazy loading 하려고 프록시 객체를 만들어 놔서 그런것 같더군요. 어떻게 막아야 하나... 고민을 했습니다.

프로젝션과 DTO로 해결하려고 했으나.. 실패. 

흠.. JSON으로 만들지 않았으면 좋겠는데.. 그런거 없나 찾아보다 @JsonIgnore 발견.
그걸 적용해서 성공했습니다.


휴.. 집에가야지
저작자 표시
신고
top


[회사일] Right 도메인 CRUD 구현 도메인 클래스부터 화면까지

프로젝트/SLT : 2010.06.14 16:21


@Entity(name="rights")
public class Right {

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

    @Column(length = 100, unique = true)
    @NotNull(message = "입력해주세요.")
    @Size(min = 1, message = "입력해주세요.")
    private String name;

    @Column
    private String descr;

    public int getId() {
        return id;
    }

    public void setId(int id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public String getDescr() {
        return descr;
    }

    public void setDescr(String descr) {
        this.descr = descr;
    }

    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;

        Right right = (Right) o;

        if (name != null ? !name.equals(right.name) : right.name != null) return false;

        return true;
    }

    @Override
    public int hashCode() {
        return name != null ? name.hashCode() : 0;
    }
}

필드 몇개 추가하고 JPA 애노테이션 추가한 다음 Member 쪽에 Set 타입으로 @ManyToMany 연결.
이때 right는 DB 예약어일 수 있으니 rights라는 이름을 사용하도록 설정 해줌. 
IDEA가 코드 생성 메뉴 상숑해서 hashCode랑 equals 구현. 이때 name 필드만 사용.

자 이제 GenericXXX를 이용해서 CRUD 코드 5분 완성.

public interface RightDao extends GenericDao<Right, RightSearchParam> {
}

@Repository
public class RightDaoImpl extends GenericDaoImpl<Right, RightSearchParam> implements RightDao {

    protected void applySearchParam(Criteria c, RightSearchParam rightSearchParam) {
        CriteriaUtils.addOptionalLike(c, "name", rightSearchParam.getName());
    }
    
}

public interface RightService extends GenericService<Right, RightSearchParam>{
}

@Service
@Transactional
public class RightServiceImpl extends GenericServiceImpl<RightDao, Right, RightSearchParam> implements RightService{

}

@Controller
@RequestMapping("/system/right")
public class RightController extends GenericController<RightService, RightRef, Right, RightSearchParam> {
}

@Component
public class RightRef {
}

public class RightSearchParam {

    private String name;

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

이걸로 서버쪽 코딩 끝. 이제 화면 코드는 태그 파일 활용해서 역시 5분 완성.

<page:editpage label="권한 수정">
    <f:input label="권한" path="name" />
    <f:textarea label="설명" path="descr" />
</page:editpage>

<page:mgtpage label="권한 관리">
    <page:search label="권한 검색">
        <s:input label="이름" path="name" />
    </page:search>
    <script type="text/javascript">
        $(function() {
            $("#smdis-grid").jqGrid({
                colNames:['id', '권한명', '설명'],
                colModel :[
                    {name:'id', index:'id', width:55, hidden:true},
                    {name:'name', index:'name', width:100},
                    {name:'descr', index:'descr', width:100}
                ]
            });
        });
    </script>
</page:mgtpage>

<page:newpage label="새 권한">
    <f:input label="이름" path="name"/>
    <f:textarea label="설명" path="descr" />
</page:newpage>

<page:viewpage label="계정 정보">
    <v:text label="id" value="${model.id}"/>
    <v:text label="이름" value="${model.name}"/>
    <v:text label="설명" value="${model.descr}"/>
</page:viewpage>

끝..


도메인 코딩 5분, 서버 코딩 5분, 화면 코딩 5분...

초간단 CRUD 구현하는데 15분.













저작자 표시
신고
top


[회사일] DateRange 추가

프로젝트/SLT : 2010.06.14 14:33



오른쪽에 보이는 검색 필드를 MemberSearchingParam에 추가하는데 아무래도 이런 범위 검색 필드는 자주 사용될 것 같더군요.

그래서 별도의 클래스로 분리했습니다.

public class DateRange {

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date from;

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date to;

...
}

나머진 요다(?) 조다(?) 라이브러리만 넣어두면 기본 포매터가 동작해서 잘 바인딩 해 줍니다. 아.. 암튼 이번엔 바인딩 얘기가 아니라..  저 클래스 DateRange 얘기입니다.

MemberSearchParam을 MemberDao코드에 적용합니다.

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

    protected void applySearchParam(Criteria c, MemberSearchParam searchParam) {
        CriteriaUtils.addOptionalLike(c, "loginId", searchParam.getLoginId());
        CriteriaUtils.addOptionalLike(c, "name", searchParam.getName());
        CriteriaUtils.addOptionalLike(c, "email", searchParam.getEmail());
        CriteriaUtils.addOptionalLike(c, "phoneNumber", searchParam.getPhoneNumber());
        CriteriaUtils.addOptionalLike(c, "jobTiTle", searchParam.getJobTitle());
        CriteriaUtils.addOptionaBetween(c, "birthDay", searchParam.getBirthDayRange());
    }
}

이게 DAO 코드 전부입니다.

유틸 코드로 가야겠네요.

 
   @Test
    public void testOptionalBetween() {
        String dateRangeSearchingFiled = "birthDay";

        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, null);
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        DateRange dateRange = new DateRange();
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(0)).add(any(Criterion.class));

        reset(c);
        dateRange.setFrom(new Date());
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(1)).add(Restrictions.ge(any(String.class), dateRange.getFrom()));

        reset(c);
        dateRange.setTo(new Date());
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(1)).add(Restrictions.between(any(String.class), dateRange.getFrom(), dateRange.getTo()));

        reset(c);
        dateRange.setFrom(null);
        CriteriaUtils.addOptionaBetween(c, dateRangeSearchingFiled, dateRange);
        verify(c, times(1)).add(Restrictions.le(any(String.class), dateRange.getTo()));
    }

장황해 보이지만 별거 없는 테스트 입니다.

  
 public static void addOptionaBetween(Criteria c, String fieldName, DateRange dateRange) {
        if(dateRange == null)
            return;

        if(dateRange.getFrom() != null && dateRange.getTo() != null){
            c.add(Restrictions.between(fieldName, dateRange.getFrom(), dateRange.getTo()));
        }

        if(dateRange.getFrom() != null && dateRange.getTo() == null){
            c.add(Restrictions.ge(fieldName, dateRange.getFrom()));
        }

        if(dateRange.getFrom() == null && dateRange.getTo() != null){
            c.add(Restrictions.le(fieldName, dateRange.getTo()));
        }
    }

이 코드를 테스트하고 있죠. 그런데 코드가 좀;; Don't Tell, Ask가 생각나더군요. 저런 조건 문을 inline 뭐시기 리팩토링으로 분리할 수도 있지만 그보다 일단 if 조건문에 들어있는 내용이 들어있어야 할 위치가... DateRange가 되어야 할 것 같단 생각이 듭니다.

머 일단 if안에 있는 조건문 들을 extract method 리팩토링으로 빼냅니다. 이런 리팩토링을 인라인 머시기 리팩토링이라고 했던것 같은데 잊어버렸네요. @_@;;

 
   public static void addOptionaBetween(Criteria c, String fieldName, DateRange dateRange) {
        if(dateRange == null)
            return;

        if(hasFromAndTo(dateRange)){
            c.add(Restrictions.between(fieldName, dateRange.getFrom(), dateRange.getTo()));
        }

        if(hasFromOnly(dateRange)){
            c.add(Restrictions.ge(fieldName, dateRange.getFrom()));
        }

        if(hasToOnly(dateRange)){
            c.add(Restrictions.le(fieldName, dateRange.getTo()));
        }
    }

    private static boolean hasToOnly(DateRange dateRange) {
        return dateRange.getFrom() == null && dateRange.getTo() != null;
    }

    private static boolean hasFromOnly(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() == null;
    }

    private static boolean hasFromAndTo(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() != null;
    }

그럼 이렇게 되는데 여기서 아래 세개의 static 메서드를 DateRange 쪽 멤버 메서드로 옯겨줍니다.

public class DateRange {

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date from;

    @DateTimeFormat(iso= DateTimeFormat.ISO.DATE)
    private Date to;

...

    public boolean hasToOnly(DateRange dateRange) {
        return dateRange.getFrom() == null && dateRange.getTo() != null;
    }

    public boolean hasFromOnly(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() == null;
    }

    public boolean hasFromAndTo(DateRange dateRange) {
        return dateRange.getFrom() != null && dateRange.getTo() != null;
    }
}

그리고 CriteriaUtils 코드를 수정해주면 되죠.

  
 public static void addOptionaBetween(Criteria c, String fieldName, DateRange dateRange) {
        if(dateRange == null)
            return;

        if(dateRange.hasFromAndTo(dateRange)){
            c.add(Restrictions.between(fieldName, dateRange.getFrom(), dateRange.getTo()));
        }

        if(dateRange.hasFromOnly(dateRange)){
            c.add(Restrictions.ge(fieldName, dateRange.getFrom()));
        }

        if(dateRange.hasToOnly(dateRange)){
            c.add(Restrictions.le(fieldName, dateRange.getTo()));
        }
    }

마지막으로 테스트 한번 돌려주면 깔끔하게 끝~









저작자 표시
신고
top

TAG DateRange

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

프로젝트/SLT : 2010.06.14 12:21


member/view.jsp

<page:viewpage label="계정 정보">
    <v:text label="id" value="${model.id}"/>
    <v:text label="로그인ID" value="${model.loginId}"/>
    <v:text label="이름" value="${model.name}"/>
    <v:text label="이메일" value="${model.email}"/>
</page:viewpage>

code/view.jsp

<page:viewpage label="코드 정보">
    <v:text label="id" value="${model.id}" />
    <v:text label="코드종류" value="${model.cate.descr}" />
    <v:text label="코드값" value="${model.code}" />
    <v:text label="코드명" value="${model.name}" />
    <v:text label="설명" value="${model.descr}" />
</page:viewpage>

흠냐.. 뷰용 태그 파일 네임스페이스는 v로 정했습니다.



자 거럼 이제 찍어낼 준비가 거의 다 된 것 같군요... 흠.. 아니군요. 아직 갈길이 멀군요.
일단 Member에 Date 타입 필드 부터 추가해봐야겠습니다.

다음에는 Right 도메인을 찍어내고, Member랑 다대다 관계 연결해서 뷰까지 또 프레임워크성 코드를 빼내야 하고...

그담엔 시큐리티 적용하고..

그담엔 자동완성 만들고.. 

엑셀 다운/업로드 만들고..

멀구나 멀어..
저작자 표시
신고
top


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

프로젝트/SLT : 2010.06.14 11:54


code/new.jsp

<page:newpage label="새 코드">
    <f:select label="코드종류" path="cate" items="${ref.codeCateList}" />
    <f:input label="코드값" path="code" />
    <f:input label="코드명" path="name" />
    <f:textarea label="설명" path="descr" />
</page:newpage>

member/new.jsp

<page:newpage label="새 계정">
    <f:input label="로그인 ID" path="loginId"/>
    <f:input label="비밀번호" path="password"/>
    <f:input label="이름" path="name"/>
    <f:input label="이메일" path="email"/>
</page:newpage>



new.jsp 페이지는 edit.jsp랑 비슷했으니까 간단간단 이제 view.jsp 페이지만 남았군요.
저작자 표시
신고
top


[회사일] 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







티스토리 툴바