Skip to content

Commit

Permalink
Merge pull request #301 from LearnsMate/feature/member
Browse files Browse the repository at this point in the history
✨기능 추가: 발급쿠폰 엑셀 다운로드, 발급 쿠폰 페이지네이션 추가
  • Loading branch information
Hellin22 authored Nov 29, 2024
2 parents 8d11873 + 7662613 commit 91c0556
Show file tree
Hide file tree
Showing 9 changed files with 411 additions and 23 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package intbyte4.learnsmate.issue_coupon.controller;

import intbyte4.learnsmate.issue_coupon.domain.dto.IssuedCouponFilterDTO;
import intbyte4.learnsmate.issue_coupon.domain.pagination.IssueCouponPageResponse;
import intbyte4.learnsmate.issue_coupon.domain.vo.request.IssueCouponFilterRequestVO;
import intbyte4.learnsmate.issue_coupon.domain.vo.response.AllIssuedCouponResponseVO;
import intbyte4.learnsmate.issue_coupon.service.IssueCouponFacade;
Expand Down Expand Up @@ -86,4 +87,40 @@ public ResponseEntity<List<AllIssuedCouponResponseVO>> findIssuedCouponsByFilter
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}

@Operation(summary = "발급된 쿠폰 전체 조회 - offset pagination")
@GetMapping("/all-issued-coupons2")
public ResponseEntity<IssueCouponPageResponse<AllIssuedCouponResponseVO>> getAllIssuedCoupons(
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "15") int size) {

IssueCouponPageResponse<AllIssuedCouponResponseVO> response;

response = issueCouponFacade.findAllIssuedCoupons(page, size);

return ResponseEntity.ok(response);
}

@Operation(summary = "발급된 쿠폰 필터링 조회 - offset pagination")
@PostMapping("/filters2")
public ResponseEntity<IssueCouponPageResponse<AllIssuedCouponResponseVO>> findIssuedCouponsByFilters(
@RequestBody IssueCouponFilterRequestVO request,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "15") int size) {
log.info("발급 쿠폰 필터링 요청 수신");
try {
IssuedCouponFilterDTO dto = issueCouponMapper.fromVOToFilterResponseDTO(request);
log.info(dto.toString());

IssueCouponPageResponse<AllIssuedCouponResponseVO> response
= issueCouponFacade.filterIssuedCoupon(page, size, dto);

log.info(response.toString());

return ResponseEntity.ok(response);
} catch (Exception e) {
log.error("예상치 못한 오류 발생", e);
return new ResponseEntity<>(null, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package intbyte4.learnsmate.issue_coupon.controller;

import intbyte4.learnsmate.issue_coupon.domain.dto.IssuedCouponFilterDTO;
import intbyte4.learnsmate.issue_coupon.service.IssueCouponExcelService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/issue-coupon/excel")
@Slf4j
@RequiredArgsConstructor
public class IssueCouponExcelController {

private final IssueCouponExcelService issueCouponExcelService;

@PostMapping("/download")
@Operation(summary = "발급쿠폰 엑셀 다운로드", description = "발급쿠폰 목록을 엑셀 파일로 다운로드합니다.")
public void downloadStudentExcel(HttpServletResponse response, @RequestBody(required = false) IssuedCouponFilterDTO filterDTO) {
try {
log.info("Excel download request received");

if (filterDTO != null) {
log.info("Filter DTO parsed: {}", filterDTO);
}
response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
response.setHeader("Content-Disposition", "attachment; filename=\"coupon_data.xlsx\"");

issueCouponExcelService.exportCouponToExcel(response.getOutputStream(), filterDTO);

log.info("Excel file successfully written to response output stream.");
} catch (Exception e) {
log.error("Error during excel download:", e);
throw new RuntimeException("Excel download failed", e);
}
}



}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import lombok.*;

import java.time.LocalDateTime;
import java.util.List;

@AllArgsConstructor
@NoArgsConstructor
Expand All @@ -30,4 +31,6 @@ public class IssuedCouponFilterDTO {
private LocalDateTime endCouponUseDate;
private LocalDateTime startCouponIssueDate;
private LocalDateTime endCouponIssueDate;

private List<String> selectedColumns;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package intbyte4.learnsmate.issue_coupon.domain.pagination;

import lombok.AllArgsConstructor;
import lombok.Getter;

import java.util.List;

@Getter
@AllArgsConstructor
public class IssueCouponPageResponse<T> {
private List<T> content;
private long totalElements;
private int totalPages;
private int currentPage;
private int size;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,14 @@

import intbyte4.learnsmate.issue_coupon.domain.IssueCoupon;
import intbyte4.learnsmate.issue_coupon.domain.vo.request.IssueCouponFilterRequestVO;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;

import java.util.List;

public interface CustomIssueCouponRepository {
List<IssueCoupon> findIssuedCouponsByFilters(IssueCouponFilterRequestVO request);

// 발급 쿠폰 offset 페이지네이션 필터링
Page<IssueCoupon> getFilteredIssuedCoupons(IssueCouponFilterRequestVO request, PageRequest pageable);
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,9 @@
import intbyte4.learnsmate.coupon.domain.entity.QCouponEntity;
import intbyte4.learnsmate.member.domain.entity.QMember;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.stereotype.Repository;

import java.util.List;
Expand Down Expand Up @@ -57,6 +60,47 @@ public List<IssueCoupon> findIssuedCouponsByFilters(IssueCouponFilterRequestVO r
.fetch();
}

// 이슈 쿠폰 필터링 offset 페이지네이션
@Override
public Page<IssueCoupon> getFilteredIssuedCoupons(IssueCouponFilterRequestVO request, PageRequest pageable) {
QIssueCoupon issueCoupon = QIssueCoupon.issueCoupon;
QMember member = QMember.member;
QCouponEntity couponEntity = QCouponEntity.couponEntity;

BooleanBuilder builder = new BooleanBuilder()
.and(likeCouponName(request))
.and(likeCouponContents(request))
.and(eqCouponCategoryName(request))
.and(eqStudentCode(request))
.and(eqStudentName(request))
.and(eqCouponUseStatus(request))
.and(betweenDiscountRate(request))
.and(betweenCouponStartDate(request))
.and(betweenCouponEndDate(request))
.and(betweenCouponUseDate(request))
.and(betweenCouponIssueDate(request));

List<IssueCoupon> results = queryFactory
.selectFrom(issueCoupon)
.join(issueCoupon.coupon, couponEntity).fetchJoin()
.join(issueCoupon.student, member).fetchJoin()
.where(builder)
.offset(pageable.getOffset())
.limit(pageable.getPageSize())
.fetch();

// 전체 카운트 조회
Long total = queryFactory
.select(issueCoupon.count())
.from(issueCoupon)
.join(issueCoupon.coupon, couponEntity)
.join(issueCoupon.student, member)
.where(builder)
.fetchOne();

return new PageImpl<>(results, pageable, total != null ? total : 0L);
}

private BooleanExpression likeCouponName(IssueCouponFilterRequestVO request) {

if (request.getCouponName() == null || request.getCouponName().isEmpty()) {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
package intbyte4.learnsmate.issue_coupon.repository;

import intbyte4.learnsmate.issue_coupon.domain.IssueCoupon;
import intbyte4.learnsmate.issue_coupon.domain.dto.IssuedCouponFilterDTO;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
Expand Down Expand Up @@ -33,4 +36,6 @@ public interface IssueCouponRepository extends JpaRepository<IssueCoupon, Long>,
@Query("SELECT c FROM issueCoupon c WHERE c.student.memberCode = :studentCode AND c.couponIssueDate <= CURRENT_TIMESTAMP AND c.couponUseStatus = true AND c.couponUseDate IS NOT NULL")
List<IssueCoupon> findUsedCouponsByStudentCode(@Param("studentCode") Long studentCode);

// 발급 쿠폰 offset 페이지네이션 전체 (JPQL 대신)
Page<IssueCoupon> findAllByOrderByCouponIssueDateDesc(PageRequest pageable);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,152 @@
package intbyte4.learnsmate.issue_coupon.service;

import intbyte4.learnsmate.issue_coupon.domain.dto.IssuedCouponFilterDTO;
import intbyte4.learnsmate.issue_coupon.domain.vo.response.AllIssuedCouponResponseVO;
import jakarta.servlet.ServletOutputStream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.poi.ss.usermodel.*;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
@RequiredArgsConstructor
public class IssueCouponExcelService {

private static final Map<String, String> COLUMNS = new LinkedHashMap<>() {{
put("couponIssuanceCode", "발급 쿠폰 번호");
put("couponName", "쿠폰 이름");
put("couponContents", "쿠폰 내용");
put("couponCategoryName", "쿠폰 종류");
put("studentCode", "고객 코드");
put("studentName", "고객 명");
put("couponUseStatus", "사용 여부");
put("couponDiscountRate", "할인율");
put("couponStartDate", "시작일");
put("couponExpireDate", "만료일");
put("couponIssueDate", "발급일");
put("couponUseDate", "사용일");
}};

private final IssueCouponFacade issueCouponFacade;

public void exportCouponToExcel(ServletOutputStream outputStream, IssuedCouponFilterDTO filterDTO) {
try (Workbook workbook = new XSSFWorkbook()) {
Sheet sheet = workbook.createSheet("Issued Coupon Data");
CellStyle headerStyle = createHeaderStyle(workbook);
CellStyle dateStyle = createDateStyle(workbook);

List<String> selectedColumns = filterDTO != null && filterDTO.getSelectedColumns() != null
? filterDTO.getSelectedColumns()
: new ArrayList<>(COLUMNS.keySet());

createHeader(sheet, headerStyle, selectedColumns);

List<AllIssuedCouponResponseVO> issueCouponList;
if (filterDTO != null) {
log.info("Applying filter with DTO: {}", filterDTO);

issueCouponList = issueCouponFacade.filterIssuedCoupon(filterDTO);
} else {
log.info("No filter applied, getting all Issued Coupons");
issueCouponList = issueCouponFacade.findAllIssuedCoupons();
}
log.info("Found {} Issued Coupons to export", issueCouponList.size());

writeData(sheet, issueCouponList, dateStyle, selectedColumns);

for (int i = 0; i < selectedColumns.size(); i++) {
sheet.autoSizeColumn(i);
}
workbook.write(outputStream);
} catch (IOException e) {
log.error("Failed to create Issued Coupon Excel file", e);
throw new RuntimeException("Failed to create Issued Coupon Excel file", e);
}
}

private void createHeader(Sheet sheet, CellStyle headerStyle, List<String> selectedColumns) {
Row headerRow = sheet.createRow(0);
int columnIndex = 0;
for (String columnKey : selectedColumns) {
Cell cell = headerRow.createCell(columnIndex++);
cell.setCellValue(COLUMNS.get(columnKey));
cell.setCellStyle(headerStyle);
}
}

private void writeData(Sheet sheet, List<AllIssuedCouponResponseVO> couponList, CellStyle dateStyle, List<String> selectedColumns) {
int rowNum = 1;
for (AllIssuedCouponResponseVO coupon : couponList) {
Row row = sheet.createRow(rowNum++);
int columnIndex = 0;
for (String key : selectedColumns) {
Cell cell = row.createCell(columnIndex++);
setValueByColumnKey(cell, key, coupon, dateStyle);
}
}
}

private void setValueByColumnKey(Cell cell, String key, AllIssuedCouponResponseVO coupon, CellStyle dateStyle) {
switch (key) {
case "couponIssuanceCode" -> cell.setCellValue(coupon.getCouponIssuanceCode());
case "couponName" -> cell.setCellValue(coupon.getCouponName());
case "couponContents" -> cell.setCellValue(coupon.getCouponContents());
case "couponCategoryName" -> cell.setCellValue(coupon.getCouponCategoryName());
case "studentCode" -> cell.setCellValue(coupon.getStudentCode());
case "studentName" -> cell.setCellValue(coupon.getStudentName());
case "couponUseStatus" -> cell.setCellValue(coupon.getCouponUseStatus() ? "사용" : "미사용");
case "couponDiscountRate" -> cell.setCellValue(coupon.getCouponDiscountRate() + "%");
case "couponStartDate" -> {
if (coupon.getCouponStartDate() != null) {
cell.setCellValue(coupon.getCouponStartDate());
cell.setCellStyle(dateStyle);
}
}
case "couponExpireDate" -> {
if (coupon.getCouponExpireDate() != null) {
cell.setCellValue(coupon.getCouponExpireDate());
cell.setCellStyle(dateStyle);
}
}
case "couponIssueDate" -> {
if (coupon.getCouponIssueDate() != null) {
cell.setCellValue(coupon.getCouponIssueDate());
cell.setCellStyle(dateStyle);
}
}
case "couponUseDate" -> {
if (coupon.getCouponUseDate() != null) {
cell.setCellValue(coupon.getCouponUseDate());
cell.setCellStyle(dateStyle);
}
}
}
}

private CellStyle createHeaderStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
style.setBorderBottom(BorderStyle.THIN);
style.setAlignment(HorizontalAlignment.CENTER);

Font font = workbook.createFont();
font.setBold(true);
style.setFont(font);
return style;
}

private CellStyle createDateStyle(Workbook workbook) {
CellStyle style = workbook.createCellStyle();
style.setDataFormat(workbook.createDataFormat().getFormat("yyyy-MM-dd HH:mm:ss"));
return style;
}
}
Loading

0 comments on commit 91c0556

Please sign in to comment.