Kotlin과 POI를 이용한 엑셀 다운로드 기능 구현하기

궁금하면 500원·2024년 7월 14일
post-thumbnail

엑셀 파일을 동적으로 생성하여 다운로드할 수 있는 기능을 구현해보겠습니다.

이 튜토리얼에서는 Kotlin을 사용하여 Apache POI 라이브러리로 엑셀 파일을 생성하는 방법을
소개합니다.

개발 환경은 IntelliJ IDEA이며, 빌드 도구로는 Gradle을 사용합니다.

1. POI 라이브러리 설치

먼저, build.gradle.kts 파일에 Apache POI 라이브러리를 추가합니다.
Apache POI는 엑셀 파일을 다루기 위한 라이브러리입니다.

dependencies {
    implementation("org.apache.poi:poi:3.17")
    implementation("org.apache.poi:poi-ooxml:3.17") // XSSF를 사용할 때 필요
}

2. Controller 설정

엑셀 파일 다운로드를 처리할 컨트롤러를 설정합니다.
이 컨트롤러는 /download/excel 경로로 요청을 받아 엑셀 파일을 반환합니다.

package kr.naver.web.controller

import kr.naver.web.service.BoardExcelView
import kr.naver.web.service.MemberExcelView
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.web.servlet.ModelAndView
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController

@RestController
class ExcelController {

    @Autowired
    private lateinit var boardExcelView: BoardExcelView

    @Autowired
    private lateinit var memberExcelView: MemberExcelView

    @GetMapping("/download/board")
    fun downloadBoardExcel(
        @RequestParam("param") param: String,
        @RequestParam("startDay") startDay: String,
        @RequestParam("endDay") endDay: String,
        @RequestParam("sheetName", defaultValue = "BoardData") sheetName: String,
        @RequestParam("fileName", defaultValue = "게시판_데이터") fileName: String
    ): ModelAndView {
        return ModelAndView(boardExcelView).apply {
            addObject("title", "게시판")
            addObject("headerTitles", listOf("번호", "내용"))
            addObject("startDate", startDay)
            addObject("endDate", endDay)
            addObject("sheetName", sheetName)
            addObject("fileName", "$fileName.xls")
        }
    }

    @GetMapping("/download/member")
    fun downloadMemberExcel(
        @RequestParam("param") param: String,
        @RequestParam("startDay") startDay: String,
        @RequestParam("endDay") endDay: String,
        @RequestParam("sheetName", defaultValue = "MemberData") sheetName: String,
        @RequestParam("fileName", defaultValue = "회원_데이터") fileName: String
    ): ModelAndView {
        return ModelAndView(memberExcelView).apply {
            addObject("title", "회원")
            addObject("headerTitles", listOf("회원ID", "이름", "가입일"))
            addObject("startDate", startDay)
            addObject("endDate", endDay)
            addObject("sheetName", sheetName)
            addObject("fileName", "$fileName.xls")
        }
    }
}

}

3. HTML 링크 추가

엑셀 다운로드 기능을 사용할 수 있도록 HTML에 링크를 추가합니다.

<a href="/download/excel">엑셀 다운로드</a>

4. View 클래스 작성

엑셀 파일을 생성하는 로직은 Service 클래스에서 구현합니다.

이 클래스는 AbstractXlsView를 상속받아 엑셀 문서를 생성하고,
스타일을 적용하는 방법을 보여줍니다.

package kr.naver.web.service

import org.apache.poi.hssf.util.HSSFColor
import org.apache.poi.ss.usermodel.*
import org.springframework.web.servlet.view.document.AbstractXlsView
import java.time.LocalDate
import java.time.format.DateTimeFormatter
import javax.servlet.http.HttpServletRequest
import javax.servlet.http.HttpServletResponse

abstract class AbstractExcelView<T> : AbstractXlsView() {

    abstract fun getData(request: HttpServletRequest): List<T>
    abstract fun fillData(sheet: Sheet, items: List<T>, bodyStyle: CellStyle)

    @Throws(Exception::class)
    override fun buildExcelDocument(model: Map<String, Any>, workbook: Workbook, request: HttpServletRequest, response: HttpServletResponse) {
        val title = model["title"] as String
        val headerTitles = model["headerTitles"] as List<String>
        val startDateStr = model["startDate"] as String
        val endDateStr = model["endDate"] as String
        val sheetName = model["sheetName"] as String? ?: "Sheet1"
        val fileName = model["fileName"] as String? ?: "$title_${LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss"))}.xls"
        val items = getData(request)

        response.setHeader("Content-Disposition", "attachment; filename=\"$fileName\"")

        val titleStyle = createTitleStyle(workbook)
        val headStyle = createHeadStyle(workbook)
        val bodyStyle = createBodyStyle(workbook)

        val sheet = workbook.createSheet(sheetName).apply {
            isDisplayGridlines = false
            autoSizeColumn(0)
        }

        createHeader(sheet, title, headerTitles, startDateStr, endDateStr, titleStyle, headStyle)
        fillData(sheet, items, bodyStyle)
        addTotalRow(sheet, items.size, bodyStyle)
    }

    private fun createTitleStyle(workbook: Workbook): CellStyle {
        return workbook.createCellStyle().apply {
            val font = workbook.createFont().apply {
                bold = true
                fontHeightInPoints = 16
            }
            setFont(font)
        }
    }

    private fun createHeadStyle(workbook: Workbook): CellStyle {
        return workbook.createCellStyle().apply {
            val font = workbook.createFont().apply {
                color = IndexedColors.WHITE.index
            }
            alignment = HorizontalAlignment.CENTER
            borderTop = BorderStyle.THIN
            borderBottom = BorderStyle.THIN
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
            fillForegroundColor = HSSFColor.HSSFColorPredefined.GREY_80_PERCENT.index
            fillPattern = FillPatternType.SOLID_FOREGROUND
            setFont(font)
        }
    }

    private fun createBodyStyle(workbook: Workbook): CellStyle {
        return workbook.createCellStyle().apply {
            dataFormat = workbook.createDataFormat().getFormat("#,##0")
            borderTop = BorderStyle.THIN
            borderBottom = BorderStyle.THIN
            borderLeft = BorderStyle.THIN
            borderRight = BorderStyle.THIN
        }
    }

    private fun createHeader(sheet: Sheet, title: String, headerTitles: List<String>, startDateStr: String, endDateStr: String, titleStyle: CellStyle, headStyle: CellStyle) {
        val row0 = sheet.createRow(0).apply {
            createCell(0).apply {
                setCellValue(title)
                cellStyle = titleStyle
            }
        }

        val row1 = sheet.createRow(1).apply {
            createCell(0).setCellValue("조회기간 : ${formatDate(startDateStr)} - ${formatDate(endDateStr)}")
        }

        val row3 = sheet.createRow(3).apply {
            headerTitles.forEachIndexed { index, headerTitle ->
                createCell(index).apply {
                    setCellValue(headerTitle)
                    cellStyle = headStyle
                }
            }
        }
    }

    private fun formatDate(dateStr: String): String {
        return try {
            LocalDate.parse(dateStr, DateTimeFormatter.ofPattern("yyyyMMdd")).format(DateTimeFormatter.ofPattern("yyyy-MM-dd"))
        } catch (e: Exception) {
            "Invalid Date"
        }
    }

    private fun addTotalRow(sheet: Sheet, itemCount: Int, bodyStyle: CellStyle) {
        val rowEnd = sheet.createRow(4 + itemCount)
        rowEnd.createCell(0).apply {
            setCellValue("총계")
            cellStyle = bodyStyle
        }
        rowEnd.createCell(1).apply {
            setCellType(CellType.NUMERIC)
            setCellValue(sheet.rowIterator().asSequence()
                .drop(4)
                .take(itemCount)
                .sumOf { row -> row.getCell(1).numericCellValue })
            cellStyle = bodyStyle
        }
    }
}

참고

Spring Boot 2.0을 이용한 엑셀 다운로드 구현 방법
스프링 부트 2.0 엑셀 다운로드 구현2
[java] Excel다운로드 기능구현3
Java Excel 엑셀 다운로드 기능 구현
[Java] POI 라이브러리 사용 : 엑셀 다운로드 로직 구현하기
POI를 이용하여 엑셀 출력하기
[POI] Excel cell style
[java] 엑셀파일쓰기 / 스타일적용
java excel poi (눈금선,병합,배경색,테두리,글씨체,사이즈,금액,천단위)
[Spring] 스프링 엑셀 POI 스타일, 폰트 적용(POI 라이브러리 2편)

결론 및 느낀점

이 튜토리얼을 통해 Kotlin과 Apache POI를 이용하여 동적으로 엑셀 파일을 생성하고
다운로드하는 방법을 배웠습니다.

실제로 데이터를 기반으로 엑셀 파일을 만들면서 셀 스타일을 설정하는 부분이 복잡할 수 있지만,
다양한 셀 스타일과 데이터 형식 설정을 통해 원하는 형식으로 엑셀 파일을 만들 수 있었습니다.

이 과정에서 POI 라이브러리의 강력함과 유연함을 느낄 수 있었습니다.

profile
에러가 나도 괜찮아 — 그건 내가 배우고 있다는 증거야.

0개의 댓글