POST /api/excel/export
Content-Type: application/json
→ attachment; filename="playground-export.xlsx"
메모 CRUD처럼 DB를 조회하는 API가 아니다.
화면(그리드)에 있는 데이터를 JSON으로 받고, 서버에서 Apache POI로 엑셀 바이너리를 만들어 파일로 내려준다.
Vue 그리드 (체크한 행)
→ JSON { sheets: [{ name, headers, rows }] }
→ Controller
→ Service (POI: Workbook → Sheet → Row → Cell)
→ byte[] (.xlsx)
→ 브라우저 다운로드
POI 계층:
Workbook (.xlsx 통째)
└ Sheet (탭 하나 — "메모", "부서" …)
└ Row (행)
└ Cell (칸)
{
"sheets": [
{
"name": "메모",
"headers": ["ID", "제목", "내용", "상태"],
"rows": [
["1", "첫 번째 메모", "연습용", "DONE"],
["2", "두 번째 메모", "테스트", "ING"]
]
}
]
}
sheets — 엑셀에 넣을 시트(탭) 목록name — 시트 이름headers — 1행 컬럼명rows — 2행부터 데이터 (문자열 배열의 배열)DB 조회 없음. Vue가 화면에 보이는/선택한 행을 그대로 보낸다.
<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로 통일한다.
시트 하나:
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.
@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[] | 파일 바이너리로 변환 |
부가:
\ / * ? : [ ] 제거, 31자 제한)ApiException 400@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);
}
}
Content-Disposition: attachment → 브라우저가 다운로드로 처리.xlsx)그리드에서 선택한 행 → 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 저장”.
POST http://localhost:8080/playground/api/excel/export
Body → raw → JSON
{
"sheets": [
{
"name": "메모",
"headers": ["ID", "제목"],
"rows": [["1", "테스트"]]
}
]
}
{ "message": "보낼 시트를 선택해 주세요" }/excel-export 페이지에서 시트 체크 후 다운로드도 동일POST /api/excel/export → Vue가 보낸 시트 데이터를 .xlsx로 만들어 다운로드Controller: @RequestBody ExcelExportRequestDto
→ Service: POI XSSFWorkbook으로 Sheet/Row/Cell 작성
→ byte[]
→ Content-Disposition: attachment + xlsx MIME
→ Vue: blob → 파일 저장
pom.xml: poi-ooxml + log4j 2.21.1 통일ExcelSheetDto / ExcelExportRequestDtobyte[]POST /api/excel/export + 다운로드 헤더| POI | 우리 코드 |
|---|---|
| xlsx 구조/포맷 | 어떤 시트·헤더·행을 넣을지 |
| Sheet / Row / Cell API | JSON 파싱 + 검증 + 스타일 |
| HTTP로 파일 내려주기 |
poi-ooxml 추가
→ log4j-api 2.21 유입
→ egov log4j-core 2.12와 충돌
→ Spring 컨텍스트 기동 실패
→ /api/memos 포함 전부 404
→ log4j-api / core / slf4j-impl을 2.21.1로 맞춤