Home Jxls merge cell 동적 병합 구현(each-merge, each-merge-single)
Post
Cancel

Jxls merge cell 동적 병합 구현(each-merge, each-merge-single)

Jxls


Jxls 는 미리 엑셀 템플릿을 만들어두고 데이터를 템플릿에 바인딩한 후 결과를 엑셀로 받게끔 처리해주는 라이브러리입니다.
엑셀 템플릿에 데이터 바인딩 할 때의 문법은 Jstl 과 매우 유사하기 때문에 Jstl 을 다뤄봤다면 조금 더 접근하기 쉽습니다.


Jxls 를 사용하며 가장 어려웠던 부분은 동적 병합 이었습니다. 때문에 두 custom 합수가 나오게 되었습니다.
each-mergeeach-merge-single 은 특징이 서로 다른 함수이기 때문에 각각 설명을 하겠습니다.


each-merge


each-merge 는 부모-자식의 관계로 동적 병합이 이루어지는 함수입니다. json 파일로 예를 들어보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
{
  "schoolList" : [
    {
      "name" : "특목고",
      "groupList" : [
        {
          "name" : "과학",
          "nameList" : [
            {
              "name" : "강바영",
              "score" : 54
            },
            {
              "name" : "윤사은",
              "score" : 65
            },
            {
              "name" : "장아린",
              "score" : 98
            },
            {
              "name" : "한자윤",
              "score" : 69
            },
            {
              "name" : "배찬영",
              "score" : 80
            }
          ]
        },
        {
          "name" : "외국어",
          "nameList" : [
            {
              "name" : "오태현",
              "score" : 79
            },
            {
              "name" : "김파은",
              "score" : 88
            },
            {
              "name" : "이하린",
              "score" : 89
            },
            {
              "name" : "박건우",
              "score" : 38
            },
            {
              "name" : "최경민",
              "score" : 78
            }
          ]
        },
        {
          "name" : "국제",
          "nameList" : [
            {
              "name" : "김두리",
              "score" : 77
            },
            {
              "name" : "이미주",
              "score" : 78
            },
            {
              "name" : "박바다",
              "score" : 99
            },
            {
              "name" : "장서연",
              "score" : 18
            },
            {
              "name" : "김수아",
              "score" : 98
            }
          ]
        }
      ]
    }
  ]
}

위 json 파일의 경우 자식 항목의 갯수 만큼 병합 시켜야 하고 결과는 아래와 같이 나옵니다.


jxls-each-merge-example1


each-merge Template


each-merge 함수를 사용한 템플릿과 함수입니다.


jxls-each-merge-example2

  1. jx:area(lastCell=”Z999”) 는 Z:999 의 범위까지 템플릿의 영역 이라는 것을 나타낸다.
  2. jx:each-merge(items=”schools” var=”school” lastCell=”D3”) 는 schools 의 리스트를 반복할 것이며, 리스트 내의 자식은 school 으로 사용하고 A:3 부터 D:3 까지 템플릿의 영역 이라는 것을 나타낸다.
  3. 2번과 마찬가지로 school의 groupList 를 반복할 것이며, 리스트 내의 자식은 group 으로 사용하고 B:3 부터 D:3 까지 템플릿의 영역 이라는 것을 나타낸다.
  4. group의 nameList 를 반복할 것이며, 리스트 내의 자식은 name 으로 사용하고 C:3 부터 D:3 까지 템플릿의 영역 이라는 것을 나타낸다.

💡 A:3 부터 D:3 까지 셀에 작성되어 있는 ${school.name} 의 형태는 자식 객체(school)가 갖고있는 항목 중 name을 반복하겠다는 의미입니다.


each-merge 구현


EachMergeCommand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class EachMergeCommand extends EachCommand {

    public static final String COMMAND_NAME = "each-merge";

    @Override
    public Size applyAt(CellRef cellRef, Context context) {


        // each-merge 셀 의 하위 셀 영역 목록을 가져온다.
        List<Area> childAreas = this.getAreaList().stream()
                .flatMap(area -> area.getCommandDataList().stream())
                .flatMap(commandData -> commandData.getCommand().getAreaList().stream())
                .collect(Collectors.toList());

        // 셀 병합을 수행하는 MergeAreaListener instance 생성
        MergeAreaListener listener = new MergeAreaListener(this.getTransformer(), cellRef);

        // each-merge comment 가 작성된 cell area 에 MergeAreaListener 추가
        this.getAreaList().get(0).addAreaListener(listener);

        //  하위 영역에 MergeAreaListener 추가
        childAreas.forEach(childArea -> {
            childArea.addAreaListener(listener);
        });

        // each 커맨드 수행
        return super.applyAt(cellRef, context);
    }
}

엑셀 내에 each-merge 함수가 있는 경우 사용하며, 한번의 each-merge 를 진행할 때마다 MergeAreaListener 생성자를 호출합니다.


MergeAreaListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
public class MergeAreaListener implements AreaListener {

    private final CellRef commandCell;

    private final Sheet sheet;

    private CellRef lastRowCellRef;

    public MergeAreaListener(Transformer transformer, CellRef cellRef) {
        this.commandCell = cellRef;
        this.sheet = ((PoiTransformer) transformer).getXSSFWorkbook().getSheet(cellRef.getSheetName());
    }


    @Override
    public void afterApplyAtCell(CellRef cellRef, Context context) {
        // child cell
        if (commandCell.getCol() != cellRef.getCol()) {
            this.setLastRowCellRef(cellRef);
        } else {
            if (existMerged(cellRef)) {
                return;
            }
            merge(cellRef);
        }
    }

    private void merge(CellRef cellRef) {
        if(this.lastRowCellRef == null) return;

        int from = cellRef.getRow();

        int lastRow = sheet.getMergedRegions().stream()
                .filter(address -> address.isInRange(this.lastRowCellRef.getRow(), this.lastRowCellRef.getCol()))
                .mapToInt(CellRangeAddressBase::getLastRow).findFirst().orElse(this.lastRowCellRef.getRow());

        log.debug("this :{}, merged start row : {} | end row : {} | col :{} ", this.toString(), from, lastRow, cellRef.getCol());

        CellRangeAddress region = new CellRangeAddress(from, lastRow, cellRef.getCol(), cellRef.getCol());
        sheet.addMergedRegion(region);
        applyStyle(sheet.getRow(cellRef.getRow()).getCell(cellRef.getCol()));
    }

    private void setLastRowCellRef(CellRef cellRef) {
        if (this.lastRowCellRef == null || this.lastRowCellRef.getRow() < cellRef.getRow()) {
            this.lastRowCellRef = cellRef;
        }
    }

    private void applyStyle(Cell cell) {
        CellStyle cellStyle = cell.getCellStyle();

        cellStyle.setAlignment(HorizontalAlignment.CENTER);
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
    }

    @Override
    public void beforeApplyAtCell(CellRef cellRef, Context context) {
    }

    @Override
    public void beforeTransformCell(CellRef srcCell, CellRef targetCell, Context context) {
    }

    @Override
    public void afterTransformCell(CellRef srcCell, CellRef targetCell, Context context) {
    }

    private boolean existMerged(CellRef cell) {
        return sheet.getMergedRegions().stream()
                .anyMatch(address -> address.isInRange(cell.getRow(), cell.getCol()));
    }

}

실제 병합이 이루어지는 함수 구현이 되어 있습니다. AreaListener 를 상속받아 afterApplyAtCell 을 재정의 했습니다.
자식의 항목 갯수만큼 반복하며 첫 번째 셀부터 마지막 셀까지 병합을 진행합니다.


each-merge-single


each-merge-single 는 단일 리스트가 동적으로 병합이 이루어지는 함수입니다. json 파일로 예를 들어보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
{
  "passList" : [
    {
      "name" : "불합격"
    },
    {
      "name" : "불합격"
    },
    {
      "name" : "합격"
    },
    {
      "name" : "불합격"
    },
    {
      "name" : "합격"
    },
    {
      "name" : "합격"
    },
    {
      "name" : "합격"
    },
    {
      "name" : "합격"
    },
    {
      "name" : "불합격"
    },
    {
      "name" : "합격"
    },
    {
      "name" : "합격"
    },
    {
      "name" : "합격"
    },
    {
      "name" : "합격"
    },
    {
      "name" : "불합격"
    },
    {
      "name" : "합격"
    }
  ]
}

위 json 파일의 경우 하나의 리스트로 중복되는 문자마다 병합 시켜야 하고 결과는 아래와 같이 나옵니다.


jxls-each-merge-single-example1


each-merge-single Template


each-merge-single 함수를 사용한 템플릿과 함수입니다.


jxls-each-merge-single-example2

  1. jx:area(lastCell=”Z999”) 는 Z:999 의 범위까지 템플릿의 영역 이라는 것을 나타낸다.
  2. jx:each-merge-single(items=”passes” var=”pass” lastCell=”A3”) 는 passes 의 리스트를 반복할 것이며, 리스트 내의 자식은 pass 으로 사용하고 A:3 까지 템플릿의 영역 이라는 것을 나타낸다.
  3. varIndex 는 반복 색인을 보유하는 Jxls context의 변수 명이며 0부터 시작합니다. (반복 시 현재 index를 알기 위해 사용했습니다)

💡 A:3 에 작성되어 있는 ${pass.name} 의 형태는 객체(pass)가 갖고있는 항목 중 name을 반복하겠다는 의미입니다.


each-merge-single 구현


EachMergeSingleCommand

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
public class EachMergeSingleCommand extends EachCommand {

    public static final String COMMAND_NAME = "each-merge-single";

    @Override
    public Size applyAt(CellRef cellRef, Context context) {
        // each-merge 셀 의 하위 셀 영역 목록을 가져온다.
        List<Area> childAreas = this.getAreaList().stream()
                .flatMap(area -> area.getCommandDataList().stream())
                .flatMap(commandData -> commandData.getCommand().getAreaList().stream())
                .collect(Collectors.toList());

        // 셀 병합을 수행하는 MergeAreaListener instance 생성
        MergeAreaSingleListener listener = new MergeAreaSingleListener(this.getTransformer(), cellRef, this.getItems());

        // each-merge comment 가 작성된 cell area 에 MergeAreaListener 추가
        this.getAreaList().get(0).addAreaListener(listener);

        //  하위 영역에 MergeAreaListener 추가
        childAreas.forEach(childArea -> {
            childArea.addAreaListener(listener);
        });

        // each 커맨드 수행
        return super.applyAt(cellRef, context);
    }
}

엑셀 내에 each-merge-single 함수가 있는 경우 사용하며, 한번의 each-merge-single 를 진행할 때마다 MergeAreaSingleListener 생성자를 호출합니다.


MergeAreaSingleListener

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
public class MergeAreaSingleListener implements AreaListener {

    private final CellRef commandCell;

    private final Sheet sheet;

    private final String item;

    private CellRef lastRowCellRef;

    private XSSFWorkbook workbook;

    public MergeAreaSingleListener(Transformer transformer, CellRef cellRef, String item) {
        this.commandCell = cellRef;
        this.workbook = ((PoiTransformer) transformer).getXSSFWorkbook();
        this.sheet = workbook.getSheet(cellRef.getSheetName());
        this.item = item;
    }


    @Override
    public void afterApplyAtCell(CellRef cellRef, Context context) {

        List<String> object = (List<String>) context.getVar(item);
        int totalSize = object.size();
        int curSize = (int) context.getVar("index");

        if(curSize + 1 == totalSize || !Objects.equals(object.get(curSize), object.get(curSize + 1))) {
            merge(cellRef);
        }else{
            return;
        }

    }

    private void merge(CellRef cellRef) {
        // 병합이 시작될 지점
        int from = this.lastRowCellRef == null ? commandCell.getRow() : this.lastRowCellRef.getRow() + 1;
        int lastRow = cellRef.getRow();

        this.setLastRowCellRef(cellRef);

        log.debug("this :{}, merged start row : {} | end row : {} | col :{} ", this.toString(), from, lastRow, cellRef.getCol());

        if(from != lastRow) {
            // 병합 cell 생성
            CellRangeAddress region = new CellRangeAddress(from, lastRow, cellRef.getCol(), cellRef.getCol());

            // sheet 에 병합된 셀 추가
            sheet.addMergedRegion(region);
        }

        // 스타일 적용
        applyStyle(sheet.getRow(from).getCell(cellRef.getCol()));
    }

    private void setLastRowCellRef(CellRef cellRef) {
        if (this.lastRowCellRef == null || this.lastRowCellRef.getRow() < cellRef.getRow()) {
            this.lastRowCellRef = cellRef;
        }
    }

    private void applyStyle(Cell cell) {
        CellStyle cellStyle = null;
        Font font = workbook.createFont();
        font.setColor(IndexedColors.WHITE.getIndex());

        // 배경, 폰트 지정
        if(cell.toString().equals("불합격")) {
            cellStyle = workbook.createCellStyle();
            cellStyle.setFont(font);
            cellStyle.setFillForegroundColor(IndexedColors.RED.getIndex());
            cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        }else if(cell.toString().equals("합격")) {
            cellStyle = workbook.createCellStyle();
            cellStyle.setFont(font);
            cellStyle.setFillForegroundColor(IndexedColors.GREEN.getIndex());
            cellStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        }else{
            cellStyle = cell.getCellStyle();
        }

        cellStyle.setAlignment(HorizontalAlignment.CENTER);
        cellStyle.setVerticalAlignment(VerticalAlignment.CENTER);
        cell.setCellStyle(cellStyle);
    }


    private boolean existMerged(CellRef cell) {
        return sheet.getMergedRegions().stream()
                .anyMatch(address -> address.isInRange(cell.getRow(), cell.getCol()));
    }

    @Override
    public void beforeApplyAtCell(CellRef cellRef, Context context) {
    }

    @Override
    public void beforeTransformCell(CellRef srcCell, CellRef targetCell, Context context) {
    }

    @Override
    public void afterTransformCell(CellRef srcCell, CellRef targetCell, Context context) {
    }

}

실제 병합이 이루어지는 함수 구현이 되어 있습니다. AreaListener 를 상속받아 afterApplyAtCell 을 재정의 했습니다.
항목 갯수만큼 반복하며 다음 항목과 다른 문자열을 가진 경우 병합을 진행합니다.
불합격인 경우 와 합격인 경우 폰트 색상과 배경색을 변경합니다.


참조



Repository


This post is licensed under CC BY 4.0 by the author.