[Playground] Spring 엑셀 다운로드 (feat. Apache POI)

Daeya·7일 전

Playground

목록 보기
9/9

Vue에서 보낸 표 데이터를 POI로 .xlsx 파일로 만들어 내려받기

POST /api/excel/export
Content-Type: application/json
→ attachment; filename="playground-export.xlsx"

메모 CRUD처럼 DB를 조회하는 API가 아니다.
화면(그리드)에 있는 데이터를 JSON으로 받고, 서버에서 Apache POI로 엑셀 바이너리를 만들어 파일로 내려준다.


1. 큰 그림 (요청 → 파일)

Vue 그리드 (체크한 행)
  → JSON { sheets: [{ name, headers, rows }] }
  → Controller
  → Service (POI: Workbook → Sheet → Row → Cell)
  → byte[] (.xlsx)
  → 브라우저 다운로드

POI 계층:

Workbook (.xlsx 통째)
  └ Sheet (탭 하나 — "메모", "부서" …)
      └ Row (행)
          └ Cell (칸)

2. 요청 JSON 형태

{
  "sheets": [
    {
      "name": "메모",
      "headers": ["ID", "제목", "내용", "상태"],
      "rows": [
        ["1", "첫 번째 메모", "연습용", "DONE"],
        ["2", "두 번째 메모", "테스트", "ING"]
      ]
    }
  ]
}
  • sheets — 엑셀에 넣을 시트(탭) 목록
  • name — 시트 이름
  • headers — 1행 컬럼명
  • rows — 2행부터 데이터 (문자열 배열의 배열)

DB 조회 없음. Vue가 화면에 보이는/선택한 행을 그대로 보낸다.


3. pom.xml — POI + log4j 버전 맞춤

<dependency>
    <groupId>org.apache.poi</groupId>
    <artifactId>poi-ooxml</artifactId>
    <version>5.2.5</version>
</dependency>

<!-- POI 5.x ↔ log4j 2.21 (egov 2.12와 혼용 시 컨텍스트/엑셀 둘 다 깨짐) -->
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-api</artifactId>
    <version>2.21.1</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-core</artifactId>
    <version>2.21.1</version>
</dependency>
<dependency>
    <groupId>org.apache.logging.log4j</groupId>
    <artifactId>log4j-slf4j-impl</artifactId>
    <version>2.21.1</version>
</dependency>

poi-ooxml만 넣으면 log4j-api 2.21이 같이 들어오고,
eGov 쪽 log4j-core 2.12버전이 섞이면 Spring 컨텍스트가 안 떠서 API가 전부 404가 난다.
log4j를 2.21.1로 통일한다.


4. 요청 DTO

시트 하나:

public class ExcelSheetDto {
    private String name;
    private List<String> headers;
    private List<List<String>> rows;
    // getter / setter
}

요청 전체:

public class ExcelExportRequestDto {
    private List<ExcelSheetDto> sheets;
    // getter / setter
}

MemoVO처럼 DB 엔티티가 아니라, 엑셀 생성용 입력 전용 DTO.


5. Service — POI로 Workbook 만들기

@Service("excelExportService")
public class ExcelExportService {

    public byte[] export(ExcelExportRequestDto request) {
        if (request.getSheets() == null || request.getSheets().isEmpty()) {
            throw new ApiException(HttpStatus.BAD_REQUEST, "보낼 시트를 선택해 주세요");
        }

        try (Workbook workbook = new XSSFWorkbook();
             ByteArrayOutputStream out = new ByteArrayOutputStream()) {

            CellStyle headerStyle = createHeaderStyle(workbook);
            CellStyle bodyStyle = createBodyStyle(workbook);

            for (ExcelSheetDto sheetDto : request.getSheets()) {
                Sheet sheet = workbook.createSheet(sanitizeSheetName(sheetDto.getName()));

                // 1행: headers
                Row headerRow = sheet.createRow(0);
                // ... cell.setCellValue(headers.get(col))

                // 2행~: rows
                // ... sheet.createRow(rowIdx + 1)

                // 열 너비 자동 조정
                // ... sheet.autoSizeColumn(col)
            }

            workbook.write(out);
            return out.toByteArray();
        } catch (IOException e) {
            throw new ApiException(HttpStatus.INTERNAL_SERVER_ERROR, "엑셀 생성에 실패했습니다");
        }
    }
}

핵심:

코드의미
new XSSFWorkbook().xlsx 워크북 생성
workbook.createSheet(name)시트(탭) 추가
sheet.createRow(n)n번째 행
row.createCell(col)칸에 값/스타일
workbook.write(out)byte[]파일 바이너리로 변환

부가:

  • 헤더 스타일 (진한 파란 배경, 흰 글씨, 테두리)
  • 시트 이름 sanitize (\ / * ? : [ ] 제거, 31자 제한)
  • 빈 sheets / 빈 headers → ApiException 400

6. Controller — 파일 다운로드 응답

@RestController
@RequestMapping("/api/excel")
public class ExcelExportController {

    @Resource(name = "excelExportService")
    private ExcelExportService excelExportService;

    @PostMapping("/export")
    public ResponseEntity<byte[]> export(@RequestBody ExcelExportRequestDto request) {
        byte[] data = excelExportService.export(request);

        return ResponseEntity.ok()
                .header(HttpHeaders.CONTENT_DISPOSITION,
                        "attachment; filename=\"playground-export.xlsx\"")
                .contentType(MediaType.parseMediaType(
                        "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
                .body(data);
    }
}
  • 응답 body = JSON이 아니라 xlsx 바이트
  • Content-Disposition: attachment → 브라우저가 다운로드로 처리
  • MIME = OpenXML spreadsheet (.xlsx)

7. Vue 쪽 (요청을 보내는 쪽)

그리드에서 선택한 행 → sheets JSON:

// ExcelExportView.vue — buildSheets()
sheets.push({
  name: grid.title,
  headers: grid.columns.map((c) => c.label),
  rows: selectedRows.map((row) =>
    grid.columns.map((c) => String(row[c.key] ?? ""))
  ),
});

다운로드:

// api/excel.js
const res = await fetch("/api/excel/export", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ sheets }),
});

const blob = await res.blob();
// <a download="playground-export-....xlsx"> 로 저장

서버는 “파일 만들기”, Vue는 “어떤 표를 보낼지 + 받은 blob 저장”.


8. Postman으로 확인

POST http://localhost:8080/playground/api/excel/export
Body → raw → JSON

{
  "sheets": [
    {
      "name": "메모",
      "headers": ["ID", "제목"],
      "rows": [["1", "테스트"]]
    }
  ]
}
  • 정상 → 200, 파일 다운로드 (또는 binary)
  • sheets 비움 → 400 { "message": "보낼 시트를 선택해 주세요" }
  • Vue /excel-export 페이지에서 시트 체크 후 다운로드도 동일

정리

기능

  • POST /api/excel/export → Vue가 보낸 시트 데이터를 .xlsx로 만들어 다운로드
  • 시트 여러 개 한 파일에 가능
  • DB 조회 없음 (화면 데이터 기반)

흐름 (요청 → 파일)

Controller: @RequestBody ExcelExportRequestDto
→ Service: POI XSSFWorkbook으로 Sheet/Row/Cell 작성
byte[]
Content-Disposition: attachment + xlsx MIME
→ Vue: blob → 파일 저장

만든 순서

  1. pom.xml: poi-ooxml + log4j 2.21.1 통일
  2. DTO: ExcelSheetDto / ExcelExportRequestDto
  3. Service: Workbook → Sheet → Row → Cell → byte[]
  4. Controller: POST /api/excel/export + 다운로드 헤더
  5. Postman / Vue 엑셀보내기 페이지 확인

POI vs 우리 코드

POI우리 코드
xlsx 구조/포맷어떤 시트·헤더·행을 넣을지
Sheet / Row / Cell APIJSON 파싱 + 검증 + 스타일
HTTP로 파일 내려주기

함정 (이번에 겪은 것)

poi-ooxml 추가
  → log4j-api 2.21 유입
  → egov log4j-core 2.12와 충돌
  → Spring 컨텍스트 기동 실패
  → /api/memos 포함 전부 404

log4j-api / core / slf4j-impl을 2.21.1로 맞춤

참고

profile
Daeya

0개의 댓글