diff --git a/src/main/java/cokr/xit/base/file/Downloadable.java b/src/main/java/cokr/xit/base/file/Downloadable.java new file mode 100644 index 0000000..55431b4 --- /dev/null +++ b/src/main/java/cokr/xit/base/file/Downloadable.java @@ -0,0 +1,15 @@ +package cokr.xit.base.file; + +import java.io.OutputStream; + +public interface Downloadable { + String getFilename(); + + String getContentType(); + + String getDisposition(); + + Number getLength(); + + void write(OutputStream out); +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/base/file/XLS.java b/src/main/java/cokr/xit/base/file/XLS.java deleted file mode 100644 index de0d4e1..0000000 --- a/src/main/java/cokr/xit/base/file/XLS.java +++ /dev/null @@ -1,229 +0,0 @@ -package cokr.xit.base.file; - -import java.io.File; -import java.io.FileInputStream; -import java.io.FileOutputStream; -import java.util.Date; -import java.util.function.Consumer; - -import org.apache.poi.ss.usermodel.Cell; -import org.apache.poi.ss.usermodel.CellStyle; -import org.apache.poi.ss.usermodel.Row; -import org.apache.poi.xssf.usermodel.XSSFSheet; -import org.apache.poi.xssf.usermodel.XSSFWorkbook; -import org.springframework.util.ResourceUtils; - -import cokr.xit.foundation.AbstractComponent; -import cokr.xit.foundation.data.StringMap; - -/**엑셀 스프레드시트의 읽기나 쓰기를 돕는 유틸리티 - * @author mjkhan - */ -public class XLS extends AbstractComponent { - /**엑셀 파일(*.xlsx)의 mime type */ - public static final String MIME_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; - - /**워크시트에 데이터셋의 각 요소(row)의 값을 쓰기 위한 Functional interface - * @author mjkhan - * @param 데이터셋의 요소(row) 유형 - */ - @FunctionalInterface - public static interface Writer { - /**워크시트에 데이터셋의 각 요소(row)의 값을 쓴다. - * @param sheet 워크시트 - * @param element 데이터셋의 각 요소(row) - * @param index 데이터셋의 요소의 인덱스. 0부터 시작 - */ - void write(XSSFSheet sheet, E element, int index); - } - - /**셀의 값을 읽어 반환한다. - * @param cell 셀 - * @return 셀의 값 - */ - public static Object getValue(Cell cell) { - switch (cell.getCellType()) { - case STRING: return cell.getStringCellValue(); - case NUMERIC: return cell.getNumericCellValue(); - case BOOLEAN: return cell.getBooleanCellValue(); - case BLANK: return ""; - default: return null; - } - } - - /**값을 셀에 설정한다. - * @param cell Cell - * @param value 값 - */ - public static void setValue(Cell cell, Object value) { - if (isEmpty(value)) { - cell.setCellValue(""); - return; - } - - if (value instanceof String) { - cell.setCellValue((String)value); - } else if (value instanceof Number) { - Number number = (Number)value; - cell.setCellValue(number.doubleValue()); - } else if (value instanceof Date) { - cell.setCellValue((Date)value); - } else if (value instanceof Boolean) { - Boolean b = (Boolean)value; - cell.setCellValue(b.booleanValue()); - } else - throw new IllegalArgumentException(value.getClass().getName() + ": " + value); - } - - private StringMap styles; - - public void setValue(String key, Cell cell, Object value) { - setValue(cell, value); - - if (styles == null) - styles = new StringMap<>(); - - CellStyle style = styles.get(key); - if (style == null) - styles.put(key, style = cell.getCellStyle()); - - cell.setCellStyle(style); - } - - /**셀의 값을 읽어 String으로 반환한다. - * @param cell 셀 - * @return 셀의 문자열값 - */ - public static String getString(Cell cell) { - Object value = getValue(cell); - return isEmpty(value) ? "" - : value instanceof String ? (String)value - : value.toString(); - } - - /**셀의 값을 읽어 Number로 반환한다. - * @param cell 셀 - * @return - *
  • 셀의 숫자값
  • - *
  • 값이 비어있으면 0
  • - *
- */ - public static Number getNumber(Cell cell) { - Object value = getValue(cell); - return isEmpty(value) ? 0 - : value instanceof Number ? (Number)value - : value instanceof String ? Double.parseDouble((String)value) - : 0; - } - - /**셀의 값을 읽어 Boolean으로 반환한다. - * @param cell 셀 - * @return - *
  • 셀의 Boolean값
  • - *
  • 값이 비어있으면 false
  • - *
- */ - public static Boolean getBoolean(Cell cell) { - Object value = getValue(cell); - return isEmpty(value) ? false - : value instanceof Boolean ? (Boolean)value - : Boolean.valueOf(value.toString()); - } - - private XSSFWorkbook workbook; - - /**FileInputStream에서 엑셀 스프레드시트를 읽어들인다. - * @param input FileInputStream - * @return XLS - */ - public XLS load(FileInputStream input) { - try { - workbook = new XSSFWorkbook(input); - return this; - } catch (Exception e) { - throw applicationException(e); - } - } - - /**지정한 경로의 파일에서 엑셀 스프레드시트를 읽어들인다. - * @param path 파일 경로. classpath:..., 또는 file:... - * @return XLS - */ - public XLS load(String path) { - try (FileInputStream input = new FileInputStream(ResourceUtils.getFile(path))) { - return load(input); - } catch (Exception e) { - throw applicationException(e); - } - } - - /**엑셀 스프레드시트의 워크북을 반환한다. - * @return 엑셀 스프레드시트의 워크북 - */ - public XSSFWorkbook getWorkbook() { - return ifEmpty(workbook, () -> workbook = new XSSFWorkbook()); - } - - /**엑셀 스프레드시트에서 지정하는 인덱스의 워크시트를 반환한다. - * @param index 워크시트 인덱스. 0부터 시작 - * @return 엑셀 스프레드시트 워크시트 - */ - public XSSFSheet getWorksheet(int index) { - int count = getWorkbook().getNumberOfSheets(); - return index < count ? workbook.getSheetAt(index) : workbook.createSheet(); - } - - /**지정하는 인덱스의 워크시트를 읽는다. - * @param sheetIndex 워크시트 인덱스. 0부터 시작 - * @param reader 워크시트의 각 row의 셀값을 읽어 작업을 수행하는 Function - * @return XLS - */ - public XLS read(int sheetIndex, Consumer reader) { - if (reader == null) return this; - - XSSFSheet worksheet = getWorksheet(sheetIndex); - for (Row row: worksheet) { - reader.accept(row); - } - return this; - } - - /**지정하는 인덱스의 워크시트에 dataset의 각 row의 값들을 쓴다. - * @param sheetIndex 워크시트 인덱스. 0부터 시작 - * @param dataset 데이터셋 - * @param writer dataset의 각 row의 값들을 워크시트에 쓰는 Function - * @return XLS - */ - public > XLS write(int sheetIndex, T dataset, Writer writer) { - XSSFSheet worksheet = getWorksheet(sheetIndex); - if (!isEmpty(dataset)) { - int index = -1; - for (E e: dataset) { - ++index; - writer.write(worksheet, e, index); - } - } - if (styles != null) { - styles.clear(); - styles = null; - } - - return this; - } - - /**지정하는 경로로 엑셀 스프레드시트를 저장하고 해당 파일을 반환한다. - * @param path 저장할 파일 경로 - * @return 스프레드시트 파일 - */ - public File write(String path) { - if (workbook == null) - throw new NullPointerException("No workbook to write"); - - try (FileOutputStream out = new FileOutputStream(path);) { - workbook.write(out); - return new File(path); - } catch (Exception e) { - throw applicationException(e); - } - } -} \ No newline at end of file diff --git a/src/main/java/cokr/xit/base/file/web/XLSView.java b/src/main/java/cokr/xit/base/file/web/XLSView.java index 3ef856c..fe1dc8d 100644 --- a/src/main/java/cokr/xit/base/file/web/XLSView.java +++ b/src/main/java/cokr/xit/base/file/web/XLSView.java @@ -1,51 +1,43 @@ package cokr.xit.base.file.web; -import java.io.InputStream; import java.net.URLEncoder; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.Map; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; -import org.jxls.common.Context; -import org.jxls.util.JxlsHelper; -import org.springframework.core.io.ClassPathResource; import org.springframework.web.servlet.view.AbstractView; -import cokr.xit.foundation.Assert; - +import cokr.xit.base.file.xls.XLS; + +/**사용자가 요청하는 데이터를 엑셀파일로 제공하는 뷰. + * 사용 순서는 다음과 같다. + * 엑셀 템플릿파일 작성. 템플릿 작성법은 + * https://jxls.sourceforge.net + * https://ddoriya.tistory.com/entry/JXLS-POI-JAVA%EC%97%90%EC%84%9C-Excel-%EC%82%AC%EC%9A%A9%ED%95%98%EB%8A%94-%EA%B5%AC%ED%98%84-%EB%B0%A9%EB%B2%95-%EB%B0%8F-%EC%A2%85%EB%A5%98-%EB%B9%84%EA%B5%90 + * 스프링 파일에 'xlsView' Bean 설정 + * 콘트롤러(*Controller.java) + * ModelAndView의 이름을 'xlsView'로 지정 + * ModelAndView에 + * 엑셀 처리기 ('xls') 설정 + * DatasetControl (JSP / *.js) + * 파라미터 설정 + * download() 호출 + * + * @author mjkhan + */ public class XLSView extends AbstractView { @Override protected void renderMergedOutputModel(Map model, HttpServletRequest hreq, HttpServletResponse hresp) throws Exception { - try (InputStream input = getInputStream(model)) { - String filename = getFilename(model); - - Context ctx = new Context(); - model.forEach(ctx::putVar); - - String charset = "UTF-8"; - hresp.setCharacterEncoding(charset); - hresp.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"); - hresp.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(filename, charset) +"\""); + XLS xls = (XLS)model.get("xls"); - JxlsHelper.getInstance().processTemplate(input, hresp.getOutputStream(), ctx); - } - } - - private InputStream getInputStream(Map model) throws Exception { - String path = (String)model.remove("template"); - if (Assert.isEmpty(path)) - throw new RuntimeException("'template' not found"); + String charset = "UTF-8"; + String filename = xls.getFilename(); - return new ClassPathResource(path).getInputStream(); - } + hresp.setCharacterEncoding(charset); + hresp.setContentType(XLS.MIME_TYPE); + hresp.setHeader("Content-Disposition", "attachment; filename=\"" + URLEncoder.encode(filename, charset) +"\""); - private String getFilename(Map model) { - return Assert.ifEmpty( - (String)model.remove("filename"), - () -> new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date()) + ".xlsx" - ); + xls.write(hresp.getOutputStream()); } } \ No newline at end of file diff --git a/src/main/java/cokr/xit/base/file/xls/JXLS.java b/src/main/java/cokr/xit/base/file/xls/JXLS.java new file mode 100644 index 0000000..57df6c7 --- /dev/null +++ b/src/main/java/cokr/xit/base/file/xls/JXLS.java @@ -0,0 +1,68 @@ +package cokr.xit.base.file.xls; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.jxls.common.Context; +import org.jxls.util.JxlsHelper; + +public class JXLS extends XLS { + private Context ctx; + private Map data; + + @Override + public JXLS setTemplate(String template) { + super.setTemplate(template); + return this; + } + + @Override + public JXLS setFilename(String filename) { + super.setFilename(filename); + return this; + } + + /**엑셀 파일에 저장할 데이터들을 반환한다. + * @return 엑셀 파일에 저장할 데이터 + */ + public Map getData() { + return data != null ? data : Collections.emptyMap(); + } + + /**엑셀 파일에 저장할 데이터들을 설정한다. + * @param map 엑셀 파일에 저장할 데이터 + * @return 현재 XLS + */ + public JXLS setData(Map map) { + data = map; + return this; + } + + /**엑셀 파일에 저장할 데이터들을 설정한다. + * @param key 데이터 키 + * @param obj 데이터 + * @return 현재 XLS + */ + public JXLS setData(String key, Object obj) { + if (data == null) { + data = new LinkedHashMap<>(); + } + data.put(key, obj); + return this; + } + + @Override + public void write(OutputStream out) { + ctx = new Context(); + getData().forEach(ctx::putVar); + + try (InputStream input = loadTemplate();) { + JxlsHelper.getInstance().processTemplate(input, out, ctx); + } catch (Exception e) { + throw runtimeException(e); + } + } +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/base/file/xls/XLS.java b/src/main/java/cokr/xit/base/file/xls/XLS.java new file mode 100644 index 0000000..2b58ccb --- /dev/null +++ b/src/main/java/cokr/xit/base/file/xls/XLS.java @@ -0,0 +1,67 @@ +package cokr.xit.base.file.xls; + +import java.io.InputStream; +import java.io.OutputStream; +import java.text.SimpleDateFormat; +import java.util.Date; + +import org.springframework.core.io.ClassPathResource; + +import cokr.xit.foundation.AbstractComponent; + +/**엑셀 스프레드시트의 읽기나 쓰기를 돕는 유틸리티 + * @author mjkhan + */ +public abstract class XLS extends AbstractComponent { + /**엑셀 파일(*.xlsx)의 mime type */ + public static final String MIME_TYPE = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"; + + private String + template, + filename; + + /**템플릿 파일경로를 반환한다. + * @return 템플릿 파일경로 + */ + public String getTemplate() { + return template; + } + + protected InputStream loadTemplate() throws Exception { + if (isEmpty(template)) + return null; + return new ClassPathResource(template).getInputStream(); + } + + /**템플릿 파일경로를 설정한다. + * @param template 템플릿 파일경로 + * @return 현재 XLS + */ + public XLS setTemplate(String template) { + this.template = template; + return this; + } + + /**저장할 파일이름을 반환한다. + * 디폴트는 'yyyyMMdd-HHmmss.xlsx' + * @return 저장할 파일이름 + */ + public String getFilename() { + return ifEmpty(filename, () -> new SimpleDateFormat("yyyyMMdd-HHmmss").format(new Date()) + ".xlsx"); + } + + /**저장할 파일이름을 설정한다. + * 디폴트는 'yyyyMMdd-HHmmss.xlsx' + * @param filename 저장할 파일이름 + * @return 현재 XLS + */ + public XLS setFilename(String filename) { + this.filename = filename; + return this; + } + + /**설정된 데이터를 엑셀 파일 포맷으로 out에 저장한다. + * @param out OutputStream + */ + public abstract void write(OutputStream out); +} \ No newline at end of file diff --git a/src/main/java/cokr/xit/base/file/xls/XLSWriter.java b/src/main/java/cokr/xit/base/file/xls/XLSWriter.java new file mode 100644 index 0000000..c6122eb --- /dev/null +++ b/src/main/java/cokr/xit/base/file/xls/XLSWriter.java @@ -0,0 +1,728 @@ +package cokr.xit.base.file.xls; + +import java.io.InputStream; +import java.io.OutputStream; +import java.util.ArrayList; +import java.util.Collections; +import java.util.Date; +import java.util.Map; +import java.util.function.Consumer; + +import org.apache.poi.ss.usermodel.BorderStyle; +import org.apache.poi.ss.usermodel.CellStyle; +import org.apache.poi.ss.usermodel.FillPatternType; +import org.apache.poi.ss.usermodel.Font; +import org.apache.poi.ss.usermodel.HorizontalAlignment; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.xssf.streaming.SXSSFCell; +import org.apache.poi.xssf.streaming.SXSSFRow; +import org.apache.poi.xssf.streaming.SXSSFSheet; +import org.apache.poi.xssf.streaming.SXSSFWorkbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; + +/**대용량 데이터를 엑셀 파일로 저장하는 유틸리티. + * XLSWriter는 엑셀 템플릿 파일을 사용하도록 할 수 있다. + *
 XLSWriter xlsx = new XLSWriter().setTemplate("...");
+ * XLSWriter가 데이터를 저장한 엑셀 파일을 다운로드할 때 사용할 이름을 지정할 수 있다. + *
 XLSWriter xlsx = new XLSWriter()
+ *     .setTemplate("...")
+ *     .setFilename("...");
+ * XLSWriter가 데이터를 엑셀로 저장하려면 워크시트와 작업셀을 지정해야 한다. + *
 xlsx.worksheet(0) //워크시트 설정
+ *     .row(0)       //첫번째 행
+ *     .col(0);      //첫번째 열의 셀을 작업셀로 설정
+ * 엑셀로 저장할 데이터를 준비한다. + *
 List<DataObject> dataset = ...;
+ * XLSWriter에 데이터를 설정한다. + *
 xlsx.values(dataset);
+ * 위 코드는 dataset의 모든 필드값들을 설정한다. + * dataset의 특정 필드의 값들을 원하는 순서대로 설정하려면 해당 필드들의 이름을 순서대로 나열한다. + *
 xlsx.values(dataset, "RCPT_ID", "RCPT_DT", "PAYER_NM", "RCPT_AMT");
+ * dataset의 특정 필드의 값들에 스타일을 적용하려면 {@link KeyStyle}을 표시한다. + *
 CellStyle
+ *     datetime = xlsx.yyyy_mm_dd_hh_mm_ss(),
+ *     numeric =  xlsx.n_nn0();
+ * xlsx.values(dataset, "RCPT_ID", xlsx.keyStyle("RCPT_DT", datetime), "PAYER_NM", xlsx.keyStyle("RCPT_AMT", numeric));
+ * @author mjkhan + */ +public class XLSWriter extends XLS { + private int rowAccessWindowSize = 128; + private SXSSFWorkbook workbook; + private SXSSFSheet worksheet; + private SXSSFRow row; + private SXSSFCell cell; + + @Override + public XLSWriter setTemplate(String template) { + super.setTemplate(template); + return this; + } + + @Override + public XLSWriter setFilename(String filename) { + super.setFilename(filename); + return this; + } + + @Override + public void write(OutputStream out) { + try { + workbook.write(out); + workbook.dispose(); + } catch (Exception e) { + throw runtimeException(e); + } finally { + clear(); + } + } + + private SXSSFWorkbook getWorkbook() { + if (workbook != null) + return workbook; + + try { + InputStream template = loadTemplate(); + if (template != null) { + XSSFWorkbook xssf = new XSSFWorkbook(template); + workbook = new SXSSFWorkbook(xssf, rowAccessWindowSize); + } else { + workbook = new SXSSFWorkbook(rowAccessWindowSize); + } + workbook.setCompressTempFiles(true); + return workbook; + } catch (Exception e) { + throw runtimeException(e); + } + } + + /**지정한 인덱스의 워크시트를 반환한다. + * 워크시트가 없으면 해당 인덱스로 워크시트를 생성 후 반환한다. + * @param index 워크시트 인덱스(0부터 시작) + * @return 워크시트 + */ + public XLSWriter worksheet(int index) { + try { + worksheet = getWorkbook().getSheetAt(index); + } catch (Exception e) { + worksheet = getWorkbook().createSheet(); + } + worksheet.setRandomAccessWindowSize(rowAccessWindowSize); + return this; + } + + /**지정한 이름의 워크시트를 반환한다. + * 워크시트가 없으면 해당 이름의 워크시트를 생성 후 반환한다. + * @param name 워크시트 이름 + * @return 워크시트 + */ + public XLSWriter worksheet(String name) { + try { + SXSSFWorkbook wb = getWorkbook(); + worksheet = ifEmpty(wb.getSheet(name), () -> wb.createSheet(name)); + worksheet.setRandomAccessWindowSize(rowAccessWindowSize); + return this; + } catch (Exception e) { + throw runtimeException(e); + } + } + + /**지정한 인덱스의 행(row)을 작업행으로 설정한다. + * 해당 행이 없으면 생성 후 작업행으로 설정한다. + * @param index 행 인덱스(0부터 시작) + * @return 현재 XLSWriter + */ + public XLSWriter row(int index) { + row = ifEmpty(worksheet.getRow(index), () -> worksheet.createRow(index)); + return this; + } + + /**현재 작업행의 지정한 인덱스의 열(column)을 작업셀로 설정한다. + * 해당 셀이 없으면 생성 후 작업셀로 설정한다. + * @param index 열 인덱스(0부터 시작) + * @return 현재 XLSWriter + */ + public XLSWriter col(int index) { + cell = ifEmpty(row.getCell(index), () -> row.createCell(index)); + return this; + } + + /**지정한 행과 열 인덱스의 셀을 작업셀로 설정한다. + * @param row 행 인덱스 + * @param col 열 인덱스 + * @return 현재 XLSWriter + */ + public XLSWriter cell(int row, int col) { + return row(row).col(col); + } + + /**지정한 값을 현재 작업셀에 설정하고 스타일을 적용한다. + * @param val 설정값 + * @param style 적용할 스타일(CellStyle, Styler, 또는 KeyStyle) + * @return 현재 XLSWriter + */ + public XLSWriter value(Object val, Object style) { + if (isEmpty(val)) + return this; + + if (val instanceof String) { + cell.setCellValue((String)val); + } else if (val instanceof Number) { + Number number = (Number)val; + cell.setCellValue(number.doubleValue()); + } else if (val instanceof Date) { + cell.setCellValue((Date)val); + } else if (val instanceof Boolean) { + Boolean b = (Boolean)val; + cell.setCellValue(b.booleanValue()); + } else + throw new IllegalArgumentException(val.getClass().getName() + ": " + val); + + setCellStyle(style); + + return this; + } + + /**지정한 값을 현재 작업셀에 설정한다. + * @param val 설정값 + * @return 현재 XLSWriter + */ + public XLSWriter value(Object val) { + return value(val, null); + } + + /**지정한 값들을 현재 작업셀부터 행으로 순서대로 설정한다.
+ * 값이 설정된 셀에 스타일을 적용하려면 해당값의 바로 뒤에 스타일을 지정한다.
+ * 다음은 사용례다. + *
 XLSWriter xlsx = new XLSWriter()
+	 *     .setTemplate("...") // 템플릿 사용 시 지정
+	 *     .setFilename("...") // 다운로드 시 파일이름 지정
+	 *     .worksheet(0)       // 워크시트 지정
+	 *     .cell(0, 0)           // 작업셀 지정
+	 *     .rowValues(List.of("하나", 10000, new Date()));
+ * 두번째 값에는 숫자포맷을, 세번째 값에는 날짜포맷을 지정하려면 다음과 같이 한다. + *
 CellStyle numeric = xlsx.n_nn0(); //{@link #cellStyle(Styler)} 참고
+	 * CellStyle datetime = xlsx.yyyy_mm_dd_hh_mm_ss();
+	 * ...
+	 * xlsx.rowValues(List.of("하나", 10000, numeric, new Date(), datetime));
+ * @param vals 설정값(스타일 포함) + * @return 현재 XLSWriter + */ + public XLSWriter rowValues(Iterable vals) { + if (!isEmpty(vals)) { + int start = cell.getColumnIndex(), + x = start; + for (Object val: vals) { + if (val instanceof Styler) { + setCellStyle(val); + } else if (val instanceof CellStyle) { + setCellStyle(val); + } else if (val instanceof KeyStyle) { + setCellStyle(val); + } else { + col(x).value(val); + ++x; + } + } + col(start); + } + return this; + } + + private void setCellStyle(Object obj) { + Styler styler = null; + CellStyle style = null; + + if (obj instanceof CellStyle) { + style = (CellStyle)obj; + } else if (obj instanceof Styler) { + styler = (Styler)obj; + } else if (obj instanceof KeyStyle) { + KeyStyle keyStyle = (KeyStyle)obj; + styler = keyStyle.styler; + style = keyStyle.style; + } + + if (styler != null) { + style = cellStyle(styler); + if (styler.width != null) { + if (styler.width < 256) + worksheet.setColumnWidth(cell.getColumnIndex(), styler.width * 256); + else + worksheet.autoSizeColumn(cell.getColumnIndex()); + } + if (styler.height != null) + row.setHeight(styler.height); + } + if (style != null) + cell.setCellStyle(style); + } + + private Iterable vals(Map map, Object[] keysAndStyles) { + if (isEmpty(map)) + return Collections.emptyList(); + + if (isEmpty(keysAndStyles)) + return map.values(); + + ArrayList vals = new ArrayList<>(); + for (Object obj: keysAndStyles) { + if (obj instanceof KeyStyle) { + KeyStyle keyStyle = (KeyStyle)obj; + Object val = map.get(keyStyle.key); + vals.add(val); + vals.add(keyStyle); + } else { + vals.add(map.get(obj)); + } + } + return vals; + } + + /**지정한 map의 값들을 현재 작업셀부터 행으로 설정한다.
+ * 설정할 값과 순서를 정하려면 해당 값들의 키를 지정한다.
+ * 값이 설정된 셀에 스타일을 적용하려면 {@link KeyStyle}로 키와 스타일을 지정한다.
+ * 다음은 map의 모든 값들을 설정하는 예다. + *
 DataObject rec = new DataObject().set("NO", 1).set("PAYER", "홍길동").set("RCPT_AMT", 10000).set("RCPT_DT", new Date());
+	 * XLSWriter xlsx = new XLSWriter()
+	 *     .setTemplate("...") // 템플릿 사용 시 지정
+	 *     .setFilename("...") // 다운로드 시 파일이름 지정
+	 *     .worksheet(0)       // 워크시트 지정
+	 *     .cell(0, 0)           // 작업셀 지정
+	 *     .rowValues(rec);
+ * 설정할 값들을 지정하려면 다음과 같이 한다. + *
 xlsx.rowValues(rec, "PAYER", "RCPT_AMT", "RCPT_DT");
+ * RCPT_AMT, RCPT_DT에 숫자포맷과 날짜포맷을 적용하려면 다음과 같이 한다. + *
 CellStyle numeric = xlsx.n_nn0(); //{@link #cellStyle(Styler)} 참고
+	 * CellStyle datetime = xlsx.yyyy_mm_dd_hh_mm_ss();
+	 * ...
+	 * xlsx.rowValues(rec, "PAYER", xlsx.keyStyle("RCPT_AMT", numeric), xlsx.keyStyle("RCPT_DT", datetime));
+ * @param map 맵 + * @param keysAndStyles 셀에 설정할 값들의 키, 또는 {@link KeyStyle 키와 스타일} + * @return 현재 XLSWriter + */ + public XLSWriter rowValues(Map map, Object... keysAndStyles) { + return rowValues(vals(map, keysAndStyles)); + } + + /**지정한 데이터셋의 값들을 현재 작업셀부터 행과 열로 설정한다. + * 설정할 값과 순서를 정하려면 해당 값들의 키를 지정한다.
+ * 값이 설정된 셀에 스타일을 적용하려면 {@link KeyStyle}로 키와 스타일을 지정한다.
+ * 다음은 데이터셋의 모든 값들을 설정하는 예다. + *
 List<DataObject> dataset = ...;
+	 * XLSWriter xlsx = new XLSWriter()
+	 *     .setTemplate("...") // 템플릿 사용 시 지정
+	 *     .setFilename("...") // 다운로드 시 파일이름 지정
+	 *     .worksheet(0)       // 워크시트 지정
+	 *     .cell(0, 0)           // 작업셀 지정
+	 *     .rowValues(dataset);
+ * 설정할 값들을 지정하려면 다음과 같이 한다. + *
 xlsx.rowValues(dataset, "PAYER", "RCPT_AMT", "RCPT_DT");
+ * RCPT_AMT, RCPT_DT에 숫자포맷과 날짜포맷을 적용하려면 다음과 같이 한다. + *
 CellStyle numeric = xlsx.n_nn0(); //{@link #cellStyle(Styler)} 참고
+	 * CellStyle datetime = xlsx.yyyy_mm_dd_hh_mm_ss();
+	 * ...
+	 * xlsx.rowValues(dataset, "PAYER", xlsx.keyStyle("RCPT_AMT", numeric), xlsx.keyStyle("RCPT_DT", datetime));
+ * @param dataset 데이터셋 + * @param keysAndStyles 셀에 설정할 값들의 키, 또는 {@link KeyStyle 키와 스타일} + * @return 현재 XLSWriter + */ + public XLSWriter values(Iterable> dataset, Object... keysAndStyles) { + if (!isEmpty(dataset)) { + int c = cell.getColumnIndex(), + r = cell.getRowIndex(); + for (Map rec: dataset) { + cell(r, c); + rowValues(rec, keysAndStyles); + ++r; + } + } + return this; + } + + public XLSWriter styles(Styler... stylers) { + if (!isEmpty(stylers)) { + int start = cell.getColumnIndex(), + x = start; + for (Styler styler: stylers) { + col(x); + setCellStyle(styler); + ++x; + } + col(start); + } + return this; + } + + /**스타일 정보에서 셀스타일을 생성하여 반환한다. + * @param styler 스타일 정보 + * @return 셀스타일 + */ + public CellStyle cellStyle(Styler styler) { + CellStyle cellStyle = workbook.createCellStyle(); + styler.set(cellStyle); + if (styler.dataFormat != null) { + cellStyle.setDataFormat(workbook.createDataFormat().getFormat(styler.dataFormat)); + } + return cellStyle; + } + + /**지정한 키와 셀스타일을 연결한 KeyStyle을 반환한다. + * @param key 키 + * @param style 셀스타일 + * @return KeyStyle + */ + public KeyStyle keyStyle(Object key, CellStyle style) { + return new KeyStyle(key, style); + } + + /**style에서 셀스타일을 생성한 후 지정한 키와 연결한 KeyStyle을 반환한다. + * @param key 키 + * @param style 스타일 정보 + * @return + */ + public KeyStyle keyStyle(Object key, Styler style) { + return keyStyle(key, cellStyle(style)); + } + + /**현재 XLSWriter의 내부상태를 초기화하여 반환한다. + * @return 현재 XLSWriter + */ + public XLSWriter clear() { + cell = null; + row = null; + worksheet = null; + workbook = null; + rowAccessWindowSize = 128; + return this; + } + + /**숫자 스타일(오른쪽 정렬, 천단위 구분)을 반환한다. + * @return 숫자 스타일(오른쪽 정렬, 천단위 구분) + */ + public CellStyle n_nn0() { + return cellStyle(new Styler().alignment(HorizontalAlignment.RIGHT).dataFormat("#,###")); + } + + /**숫자 스타일(오른쪽 정렬, 천단위 구분, 소수점 2자리 표기)을 반환한다. + * @return 숫자 스타일(오른쪽 정렬, 천단위 구분, 소수점 2자리 표기) + */ + public CellStyle n_nn0_2() { + return cellStyle(new Styler().dataFormat("#,###.00").merge(Styler.RIGHT)); + } + + /**날짜 포맷(yyyy-MM-dd HH:mm:ss)의 셀스타일을 반환한다. + * @return 날짜 포맷(yyyy-MM-dd HH:mm:ss)의 셀스타일 + */ + public CellStyle yyyy_mm_dd_hh_mm_ss() { + return cellStyle(new Styler().dataFormat("yyyy-MM-dd HH:mm:ss").merge(Styler.CENTER)); + } + + /**날짜 포맷(yyyy-MM-dd)의 셀스타일을 반환한다. + * @return 날짜 포맷(yyyy-MM-dd)의 셀스타일 + */ + public CellStyle yyyy_mm_dd() { + return cellStyle(new Styler().dataFormat("yyyy-MM-dd").merge(Styler.CENTER)); + } + + /**셀스타일 정보. {@link #set(CellStyle) CellStyle을 설정}하는데 사용. + * @author mjkhan + */ + public static class Styler { + /** 빈 스타일 */ + public static final Styler NONE = new Styler().seal(); + /** 좌측 정렬 */ + public static final Styler LEFT = new Styler().alignment(HorizontalAlignment.LEFT).seal(); + /** 가운데 정렬 */ + public static final Styler CENTER = new Styler().alignment(HorizontalAlignment.CENTER).seal(); + /** 우측 정렬 */ + public static final Styler RIGHT = new Styler().alignment(HorizontalAlignment.RIGHT).seal(); + + private boolean sealed; + + private Integer width; + private Short height; + + private HorizontalAlignment horizontalAlignment; + private VerticalAlignment verticalAlignment; + + private Border + borderTop, + borderRight, + borderBottom, + borderLeft; + + private Short foregroundColor; + + private Font font; + private String dataFormat; + + /**열의 폭을 설정한다. + * @param chars 열의 폭. 한번에 보여지는 문자수 + * @return 현재 Styler + */ + public Styler width(int chars) { + this.width = chars; + return this; + } + + /**행의 높이를 설정한다. + * @param height 행의 높이 + * @return 현재 Styler + */ + public Styler height(short height) { + this.height = height; + return this; + } + + /**가로정렬을 설정한다. + * @param alignment 가로정렬 + * @return 현재 Styler + */ + public Styler alignment(HorizontalAlignment alignment) { + ensureNotSealed(); + horizontalAlignment = alignment; + return this; + } + + /**세로정렬을 설정한다. + * @param alignment 세로정렬 + * @return 현재 Styler + */ + public Styler alignment(VerticalAlignment alignment) { + ensureNotSealed(); + verticalAlignment = alignment; + return this; + } + + /**윗쪽 테두리 스타일을 설정한다. + * @param border 테두리 스타일 정보 + * @return 현재 Styler + */ + public Styler borderTop(Border border) { + ensureNotSealed(); + borderTop = border; + return this; + } + + /**오른쪽 테두리 스타일을 설정한다. + * @param border 테두리 스타일 정보 + * @return 현재 Styler + */ + public Styler borderRight(Border border) { + ensureNotSealed(); + borderRight = border; + return this; + } + + /**아래쪽 테두리 스타일을 설정한다. + * @param border 테두리 스타일 정보 + * @return 현재 Styler + */ + public Styler borderBottom(Border border) { + ensureNotSealed(); + borderBottom = border; + return this; + } + + /**왼쪽 테두리 스타일을 설정한다. + * @param border 테두리 스타일 정보 + * @return 현재 Styler + */ + public Styler borderLeft(Border border) { + ensureNotSealed(); + borderLeft = border; + return this; + } + + /**배경색을 설정한다. + * @param color + * @return 현재 Styler + */ + public Styler foregroundColor(short color) { + ensureNotSealed(); + foregroundColor = color; + return this; + } + + /**폰트를 설정한다. + * @param font 폰트 + * @return 현재 Styler + */ + public Styler font(Font font) { + ensureNotSealed(); + this.font = font; + return this; + } + + /**데이터 포맷을 설정한다. + * @param dataFormat 데이터 포맷 + * @return 현재 Styler + */ + public Styler dataFormat(String dataFormat) { + ensureNotSealed(); + this.dataFormat = dataFormat; + return this; + } + + /**셀스타일에 현재 스타일 정보를 설정한다. + * @param style 셀스타일 + */ + public void set(CellStyle style) { + if (style == null) return; + + if (horizontalAlignment != null) + style.setAlignment(horizontalAlignment); + if (verticalAlignment != null) + style.setVerticalAlignment(verticalAlignment); + + if (borderTop != null) { + if (borderTop.style != null) + style.setBorderTop(borderTop.style); + if (borderTop.color != null) + style.setTopBorderColor(borderTop.color); + } + if (borderRight != null) { + if (borderRight.style != null) + style.setBorderRight(borderRight.style); + if (borderRight.color != null) + style.setRightBorderColor(borderRight.color); + } + if (borderBottom != null) { + if (borderBottom.style != null) + style.setBorderBottom(borderBottom.style); + if (borderBottom.color != null) + style.setBottomBorderColor(borderBottom.color); + } + if (borderLeft != null) { + if (borderLeft.style != null) + style.setBorderLeft(borderLeft.style); + if (borderLeft.color != null) + style.setLeftBorderColor(borderLeft.color); + } + if (foregroundColor != null) { + style.setFillForegroundColor(foregroundColor); + style.setFillPattern(FillPatternType.SOLID_FOREGROUND); + } + + if (font != null) + style.setFont(font); + } + + public Styler configure(Consumer configurer) { + ensureNotSealed(); + if (configurer != null) + configurer.accept(this); + return this; + } + + public Styler merge(Styler other) { + Styler copy = new Styler(); + + copy.width = this.width; + copy.height = this.height; + + copy.horizontalAlignment = this.horizontalAlignment; + copy.verticalAlignment = this.verticalAlignment; + + copy.borderTop = this.borderTop; + copy.borderRight = this.borderRight; + copy.borderBottom = this.borderBottom; + copy.borderLeft = this.borderLeft; + + copy.foregroundColor = this.foregroundColor; + copy.font = this.font; + copy.dataFormat = this.dataFormat; + + if (other.width != null) + copy.width = other.width; + if (other.height != null) + copy.height = other.height; + + if (other.horizontalAlignment != null) + copy.horizontalAlignment = other.horizontalAlignment; + if (other.verticalAlignment != null) + copy.verticalAlignment = other.verticalAlignment; + + if (other.borderTop != null) + copy.borderTop = other.borderTop; + if (other.borderRight != null) + copy.borderRight = other.borderRight; + if (other.borderBottom != null) + copy.borderBottom = other.borderBottom; + if (other.borderLeft != null) + copy.borderLeft = other.borderLeft; + + if (other.foregroundColor != null) + copy.foregroundColor = other.foregroundColor; + if (other.font != null) + copy.font = other.font; + if (other.dataFormat != null) + copy.dataFormat = other.dataFormat; + + return copy; + } + + private Styler seal() { + sealed = true; + return this; + } + + private void ensureNotSealed() { + if (sealed) + throw new RuntimeException("The Style is sealed and unmodifieable"); + } + } + + /**테두리 스타일 정보 + * @author mjkhan + */ + public static class Border { + private BorderStyle style; + private Short color; + + /**테두리 스타일 정보를 설정한다. + * @param style 테두리 스타일 정보 + * @return 현재 테두리 스타일 정보 + */ + public Border style(BorderStyle style) { + this.style = style; + return this; + } + + /**테두리 색깔을 설정한다. + * @param color 테두리 색깔 + * @return 현재 테두리 스타일 정보 + */ + public Border color(short color) { + this.color = color; + return this; + } + } + + /**셀스타일을 특정키와 연결하는 정보 + * @author mjkhan + */ + public static class KeyStyle { + private Object key; + private CellStyle style; + private Styler styler; + + /**새 KeyStyle을 생성한다. + * @param key 키 + * @param style 셀스타일 + */ + KeyStyle(Object key, CellStyle style) { + this.key = notEmpty(key, "key"); + this.style = style; + } + + /**새 KeyStyle을 생성한다. + * @param key 키 + * @param styler 스타일러 + */ + KeyStyle(Object key, Styler styler) { + this.key = notEmpty(key, "key"); + this.styler = styler; + } + } +} \ No newline at end of file