Jxls
Jxls 는 미리 엑셀 템플릿을 만들어두고 데이터를 템플릿에 바인딩한 후 결과를 엑셀로 받게끔 처리해주는 라이브러리입니다.
엑셀 템플릿에 데이터 바인딩 할 때의 문법은 Jstl 과 매우 유사하기 때문에 Jstl 을 다뤄봤다면 조금 더 접근하기 쉽습니다.
Jxls 를 사용하며 가장 어려웠던 부분은 동적 병합 이었습니다. 때문에 두 custom 합수가 나오게 되었습니다.
each-merge
와each-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 파일의 경우 자식 항목의 갯수 만큼 병합 시켜야 하고 결과는 아래와 같이 나옵니다.
each-merge Template
each-merge 함수를 사용한 템플릿과 함수입니다.
- jx:area(lastCell=”Z999”) 는 Z:999 의 범위까지 템플릿의 영역 이라는 것을 나타낸다.
- jx:each-merge(items=”schools” var=”school” lastCell=”D3”) 는 schools 의 리스트를 반복할 것이며, 리스트 내의 자식은 school 으로 사용하고 A:3 부터 D:3 까지 템플릿의 영역 이라는 것을 나타낸다.
- 2번과 마찬가지로 school의 groupList 를 반복할 것이며, 리스트 내의 자식은 group 으로 사용하고 B:3 부터 D:3 까지 템플릿의 영역 이라는 것을 나타낸다.
- 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 파일의 경우 하나의 리스트로 중복되는 문자마다 병합 시켜야 하고 결과는 아래와 같이 나옵니다.
each-merge-single Template
each-merge-single 함수를 사용한 템플릿과 함수입니다.
- jx:area(lastCell=”Z999”) 는 Z:999 의 범위까지 템플릿의 영역 이라는 것을 나타낸다.
- jx:each-merge-single(items=”passes” var=”pass” lastCell=”A3”) 는 passes 의 리스트를 반복할 것이며, 리스트 내의 자식은 pass 으로 사용하고 A: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 을 재정의 했습니다.
항목 갯수만큼 반복하며 다음 항목과 다른 문자열을 가진 경우 병합을 진행합니다.
불합격
인 경우 와 합격
인 경우 폰트 색상과 배경색을 변경합니다.