[회사일] GenericExcelView 만들기

현재 업무를 엑셀 파일로 처리하고 있기 때문에 엑셀 파일과의 연동을 필수적입니다. 따라서 시스템에 들어있는 정보를 엑셀로 받아볼 수 있게 하거나 엑셀 정보를 시스템에 올려서 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 쪽에도 기능을 추가해줬는데 그거야 뭐 간단하니깐. 생략하겠습니다. 이제 재고관리와 상품관리쪽에 엑셀 업로드를 만들어야겠습니다. 내일할까나;; 흠.

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

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

원래 이 회사에서 다루는 재고가 ‘신발’ 하나였는데 이제 곧 ‘가방’까지 늘어날 예정인가 봅니다. 고객(울 회사 대표님)과 대화를 하지 않았다면 몰랐을텐데.. 초기에 알았으니 그나마 다행입니다. 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도 추가해봐야겠습니다.

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

팝업은 팬시박스를 사용했습니다. 이게 저번에 선택했던 컬러 머시기 보다 더 깔끔하더군요.
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);
이렇게 될텐데 말이죠. 흠.. 뭐좀 먹고 와서 해봐야지.

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

계정 정보 뷰 화면에 권한 정보를 뿌리도록 서브 그리드를 추가합니다.
화면 코드에서는 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 발견.
그걸 적용해서 성공했습니다.
휴.. 집에가야지

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

@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분.