From 6ca0bd1c42b23ac57608e8c4199a15aebeaa0caf Mon Sep 17 00:00:00 2001 From: dradnats1012 Date: Sun, 6 Oct 2024 00:00:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=98=81=EC=96=91=EC=82=AC=EB=8B=98=20?= =?UTF-8?q?=EC=97=91=EC=85=80=20=EB=8B=A4=EC=9A=B4=EB=A1=9C=EB=93=9C=20?= =?UTF-8?q?=EA=B8=B0=EB=8A=A5=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 + .../koin/domain/coop/controller/CoopApi.java | 22 ++++ .../coop/controller/CoopController.java | 18 +++ .../domain/coop/dto/ExcelResponseBuilder.java | 26 ++++ .../koin/domain/coop/service/CoopService.java | 122 +++++++++++++++++- .../dining/repository/DiningRepository.java | 4 + 6 files changed, 195 insertions(+), 1 deletion(-) create mode 100644 src/main/java/in/koreatech/koin/domain/coop/dto/ExcelResponseBuilder.java diff --git a/build.gradle b/build.gradle index 355bb28b0..7160bbcc9 100644 --- a/build.gradle +++ b/build.gradle @@ -88,6 +88,10 @@ dependencies { annotationProcessor "com.querydsl:querydsl-apt:5.0.0:jakarta" annotationProcessor "jakarta.annotation:jakarta.annotation-api" annotationProcessor "jakarta.persistence:jakarta.persistence-api" + + // Excel + implementation 'org.apache.poi:poi-ooxml:5.2.5' + implementation 'com.opencsv:opencsv:5.9' } clean { diff --git a/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java b/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java index 0adaf55f7..ed8c74426 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java +++ b/src/main/java/in/koreatech/koin/domain/coop/controller/CoopApi.java @@ -2,11 +2,17 @@ import static in.koreatech.koin.domain.user.model.UserType.COOP; +import java.time.LocalDate; + +import org.springframework.core.io.InputStreamResource; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; 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.RequestParam; import in.koreatech.koin.domain.coop.dto.CoopLoginRequest; import in.koreatech.koin.domain.coop.dto.CoopLoginResponse; @@ -70,4 +76,20 @@ ResponseEntity saveDiningImage( ResponseEntity coopLogin( @RequestBody @Valid CoopLoginRequest request ); + + @ApiResponses( + value = { + @ApiResponse(responseCode = "201"), + @ApiResponse(responseCode = "400", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "403", content = @Content(schema = @Schema(hidden = true))), + @ApiResponse(responseCode = "404", content = @Content(schema = @Schema(hidden = true))), + } + ) + @Operation(summary = "영양사 식단 엑셀 다운로드") + @GetMapping("/dining/excel") + ResponseEntity generateCoopExcel( + @RequestParam("startDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam("endDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + @RequestParam("isCafeteria") Boolean isCafeteria + ); } diff --git a/src/main/java/in/koreatech/koin/domain/coop/controller/CoopController.java b/src/main/java/in/koreatech/koin/domain/coop/controller/CoopController.java index 6790b9b44..e9c46bc7b 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/controller/CoopController.java +++ b/src/main/java/in/koreatech/koin/domain/coop/controller/CoopController.java @@ -2,18 +2,25 @@ import static in.koreatech.koin.domain.user.model.UserType.COOP; +import java.io.ByteArrayInputStream; import java.net.URI; +import java.time.LocalDate; +import org.springframework.core.io.InputStreamResource; +import org.springframework.format.annotation.DateTimeFormat; import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PatchMapping; 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.RequestParam; import org.springframework.web.bind.annotation.RestController; import in.koreatech.koin.domain.coop.dto.CoopLoginRequest; import in.koreatech.koin.domain.coop.dto.CoopLoginResponse; import in.koreatech.koin.domain.coop.dto.DiningImageRequest; +import in.koreatech.koin.domain.coop.dto.ExcelResponseBuilder; import in.koreatech.koin.domain.coop.dto.SoldOutRequest; import in.koreatech.koin.domain.coop.service.CoopService; import in.koreatech.koin.global.auth.Auth; @@ -53,4 +60,15 @@ public ResponseEntity coopLogin( return ResponseEntity.created(URI.create("/")) .body(response); } + + @GetMapping("/dining/excel") + public ResponseEntity generateCoopExcel( + @RequestParam("startDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate startDate, + @RequestParam("endDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate, + @RequestParam(name = "isCafeteria", defaultValue = "false") Boolean isCafeteria + ) { + ByteArrayInputStream excelFile = coopService.generateCoopExcel(startDate, endDate, isCafeteria); + return ExcelResponseBuilder.buildExcelResponse(excelFile, startDate, endDate); + + } } diff --git a/src/main/java/in/koreatech/koin/domain/coop/dto/ExcelResponseBuilder.java b/src/main/java/in/koreatech/koin/domain/coop/dto/ExcelResponseBuilder.java new file mode 100644 index 000000000..73992ed8f --- /dev/null +++ b/src/main/java/in/koreatech/koin/domain/coop/dto/ExcelResponseBuilder.java @@ -0,0 +1,26 @@ +package in.koreatech.koin.domain.coop.dto; + +import java.io.ByteArrayInputStream; +import java.time.LocalDate; + +import org.springframework.core.io.InputStreamResource; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; + +public class ExcelResponseBuilder { + + public static ResponseEntity buildExcelResponse( + ByteArrayInputStream excelFile, LocalDate startDate, LocalDate endDate + ) { + String fileName = startDate.toString() + " ~ " + endDate.toString() + " menu.xlsx"; + + HttpHeaders headers = new HttpHeaders(); + headers.add("Content-Disposition", "attachment; filename=" + fileName); + + return ResponseEntity.ok() + .headers(headers) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .body(new InputStreamResource(excelFile)); + } +} diff --git a/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java b/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java index 353998a97..2410b44b3 100644 --- a/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java +++ b/src/main/java/in/koreatech/koin/domain/coop/service/CoopService.java @@ -1,9 +1,27 @@ package in.koreatech.koin.domain.coop.service; +import java.io.ByteArrayInputStream; +import java.io.ByteArrayOutputStream; +import java.io.IOException; import java.time.Clock; +import java.time.LocalDate; import java.time.LocalDateTime; +import java.util.Arrays; +import java.util.List; import java.util.UUID; +import java.util.concurrent.atomic.AtomicInteger; +import org.apache.poi.ss.usermodel.Cell; +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.IndexedColors; +import org.apache.poi.ss.usermodel.Row; +import org.apache.poi.ss.usermodel.Sheet; +import org.apache.poi.ss.usermodel.VerticalAlignment; +import org.apache.poi.ss.usermodel.Workbook; +import org.apache.poi.xssf.usermodel.XSSFWorkbook; import org.springframework.context.ApplicationEventPublisher; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -44,6 +62,8 @@ public class CoopService { private final PasswordEncoder passwordEncoder; private final JwtProvider jwtProvider; + private final int CELL_NUM = 8; + @Transactional public void changeSoldOut(SoldOutRequest soldOutRequest) { Dining dining = diningRepository.getById(soldOutRequest.menuId()); @@ -53,7 +73,8 @@ public void changeSoldOut(SoldOutRequest soldOutRequest) { dining.setSoldOut(now); boolean isOpened = coopShopService.getIsOpened(now, CoopShopType.CAFETERIA, dining.getType(), false); if (isOpened && diningSoldOutCacheRepository.findById(dining.getPlace()).isEmpty()) { - eventPublisher.publishEvent(new DiningSoldOutEvent(dining.getId(), dining.getPlace(), dining.getType())); + eventPublisher.publishEvent( + new DiningSoldOutEvent(dining.getId(), dining.getPlace(), dining.getType())); } } else { dining.cancelSoldOut(); @@ -90,4 +111,103 @@ public CoopLoginResponse coopLogin(CoopLoginRequest request) { return CoopLoginResponse.of(accessToken, savedToken.getRefreshToken()); } + + public ByteArrayInputStream generateCoopExcel(LocalDate startDate, LocalDate endDate, Boolean isCafeteria) { + List dinings; + + if(isCafeteria) { + List placeFilters = Arrays.asList("A코너", "B코너", "C코너"); + dinings = diningRepository.findByDateBetweenAndPlaceIn(startDate, endDate, placeFilters); + } else { + dinings = diningRepository.findByDateBetween(startDate, endDate); + } + + + try (Workbook workbook = new XSSFWorkbook()) { + Sheet sheet = workbook.createSheet("식단 메뉴"); + + CellStyle headerStyle = makeHeaderStyle(workbook); + CellStyle commonStyle = makeCommonStyle(workbook); + createHeaderCell(sheet, headerStyle); + + return putDiningData(dinings, sheet, commonStyle, workbook); + + } catch (IOException e) { + throw new RuntimeException("엑셀 파일 생성 중 오류가 발생했습니다.", e); + } + } + + private ByteArrayInputStream putDiningData(List dinings, Sheet sheet, CellStyle commonStyle, + Workbook workbook) throws IOException { + AtomicInteger rowIdx = new AtomicInteger(1); + + dinings.forEach(dining -> { + Row row = sheet.createRow(rowIdx.getAndIncrement()); + row.createCell(0).setCellValue(dining.getDate().toString()); + row.createCell(1).setCellValue(dining.getType().toString()); + row.createCell(2).setCellValue(dining.getPlace()); + row.createCell(3).setCellValue(dining.getKcal() != null ? dining.getKcal() : 0); + + String formattedMenu = dining.getMenu().toString() + .replaceAll("^\\[|\\]$", "") + .replaceAll(", ", "\n"); + + Cell menuCell = row.createCell(4); + menuCell.setCellValue(formattedMenu); + + row.createCell(5).setCellValue(dining.getImageUrl()); + row.createCell(6).setCellValue(dining.getSoldOut()); + row.createCell(7).setCellValue(dining.getIsChanged()); + + for (int i = 0; i < 8; i++) { + row.getCell(i).setCellStyle(commonStyle); + } + }); + + for (int i = 0; i < CELL_NUM; i++) { + sheet.autoSizeColumn(i); + sheet.setColumnWidth(i, (sheet.getColumnWidth(i) + 1024)); + } + + ByteArrayOutputStream out = new ByteArrayOutputStream(); + workbook.write(out); + return new ByteArrayInputStream(out.toByteArray()); + } + + private void createHeaderCell(Sheet sheet, CellStyle headerStyle) { + Row headerRow = sheet.createRow(0); + headerRow.createCell(0).setCellValue("날짜"); + headerRow.createCell(1).setCellValue("타입"); + headerRow.createCell(2).setCellValue("코너"); + headerRow.createCell(3).setCellValue("칼로리"); + headerRow.createCell(4).setCellValue("메뉴"); + headerRow.createCell(5).setCellValue("이미지"); + headerRow.createCell(6).setCellValue("품절 여부"); + headerRow.createCell(7).setCellValue("변경 여부"); + + for (int i = 0; i < CELL_NUM; i++) { + Cell cell = headerRow.getCell(i); + cell.setCellStyle(headerStyle); + } + } + + private static CellStyle makeCommonStyle(Workbook workbook) { + CellStyle commonStyle = workbook.createCellStyle(); + commonStyle.setAlignment(HorizontalAlignment.CENTER); + commonStyle.setVerticalAlignment(VerticalAlignment.CENTER); + commonStyle.setWrapText(true); + return commonStyle; + } + + private static CellStyle makeHeaderStyle(Workbook workbook) { + CellStyle headerStyle = workbook.createCellStyle(); + Font font = workbook.createFont(); + font.setBold(true); + font.setColor(IndexedColors.WHITE.getIndex()); + headerStyle.setFont(font); + headerStyle.setFillForegroundColor(IndexedColors.LIGHT_BLUE.getIndex()); + headerStyle.setFillPattern(FillPatternType.SOLID_FOREGROUND); + headerStyle.setAlignment(HorizontalAlignment.CENTER); + return headerStyle; + } } diff --git a/src/main/java/in/koreatech/koin/domain/dining/repository/DiningRepository.java b/src/main/java/in/koreatech/koin/domain/dining/repository/DiningRepository.java index 9587f8b26..c43547c07 100644 --- a/src/main/java/in/koreatech/koin/domain/dining/repository/DiningRepository.java +++ b/src/main/java/in/koreatech/koin/domain/dining/repository/DiningRepository.java @@ -32,4 +32,8 @@ default Dining getById(Integer id) { Long count(); Page findAllByMenuContainingAndPlaceIn(String keyword, List diningPlaces, Pageable pageable); + + List findByDateBetween(LocalDate startDate, LocalDate endDate); + + List findByDateBetweenAndPlaceIn(LocalDate startDate, LocalDate endDate, List placeFilters); }