Skip to content

Commit daf2f21

Browse files
authored
Merge pull request #190 from GoTogether-Inc/feature/#184
[feat] 구매/참가자 관리 목록 엑셀 다운로드 API 구현
2 parents 9257367 + 85a65af commit daf2f21

8 files changed

Lines changed: 214 additions & 1 deletion

File tree

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,9 @@ dependencies {
6868
// Prometheus & Actuator
6969
implementation 'org.springframework.boot:spring-boot-starter-actuator'
7070
implementation 'io.micrometer:micrometer-registry-prometheus'
71+
72+
// POI
73+
implementation 'org.apache.poi:poi-ooxml:5.2.3'
7174
}
7275

7376
tasks.named('test') {

src/main/java/com/gotogether/domain/hostchannel/controller/HostChannelApi.java

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -295,4 +295,25 @@ ApiResponse<?> approveOrderStatus(
295295
@Parameter(description = "주문 상태 변경 요청 데이터", required = true)
296296
@RequestBody @Valid OrderStatusRequestDTO request
297297
);
298+
299+
@Operation(
300+
summary = "구매/참가자 관리 목록 엑셀 다운로드",
301+
description = "구매/참가자 관리 목록을 엑셀 형식으로 다운로드합니다."
302+
)
303+
@ApiResponses(value = {
304+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
305+
responseCode = "200",
306+
description = "구매/참가자 관리 목록 엑셀 다운로드 성공",
307+
content = @Content(schema = @Schema(implementation = org.springframework.core.io.Resource.class))
308+
),
309+
@io.swagger.v3.oas.annotations.responses.ApiResponse(
310+
responseCode = "404",
311+
description = "EVENT4001: 이벤트가 없습니다."
312+
)
313+
})
314+
@GetMapping("/dashboard/participant-management/excel")
315+
org.springframework.http.ResponseEntity<org.springframework.core.io.Resource> exportParticipantManagementExcel(
316+
@Parameter(description = "이벤트 ID", required = true)
317+
@RequestParam Long eventId
318+
);
298319
}

src/main/java/com/gotogether/domain/hostchannel/controller/HostChannelController.java

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,9 @@
3333
import jakarta.validation.Valid;
3434
import lombok.RequiredArgsConstructor;
3535

36+
import java.net.URLEncoder;
37+
import java.nio.charset.StandardCharsets;
38+
3639
@RestController
3740
@RequiredArgsConstructor
3841
@RequestMapping("/api/v1/host-channels")
@@ -132,4 +135,20 @@ public ApiResponse<?> approveOrderStatus(
132135
hostChannelService.approveOrderStatus(request.getOrderId());
133136
return ApiResponse.onSuccess("주문 승인 완료");
134137
}
138+
139+
@Override
140+
public org.springframework.http.ResponseEntity<org.springframework.core.io.Resource> exportParticipantManagementExcel(
141+
@RequestParam Long eventId
142+
) {
143+
byte[] excelData = hostChannelService.generateParticipantManagementExcel(eventId);
144+
String fileName = hostChannelService.generateParticipantManagementExcelFileName(eventId);
145+
String encodedFileName = URLEncoder.encode(fileName, StandardCharsets.UTF_8)
146+
.replaceAll("\\+", "%20");
147+
148+
return org.springframework.http.ResponseEntity.ok()
149+
.header(org.springframework.http.HttpHeaders.CONTENT_DISPOSITION,
150+
"attachment; filename*=UTF-8''" + encodedFileName)
151+
.contentType(org.springframework.http.MediaType.parseMediaType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet"))
152+
.body(new org.springframework.core.io.ByteArrayResource(excelData));
153+
}
135154
}

src/main/java/com/gotogether/domain/hostchannel/service/HostChannelService.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,4 +39,8 @@ List<ParticipantManagementResponseDTO> getParticipantManagement(Long eventId, St
3939
HostDashboardResponseDTO getHostDashboard(Long eventId);
4040

4141
void approveOrderStatus(Long orderId);
42+
43+
byte[] generateParticipantManagementExcel(Long eventId);
44+
45+
String generateParticipantManagementExcelFileName(Long eventId);
4246
}

src/main/java/com/gotogether/domain/hostchannel/service/HostChannelServiceImpl.java

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,9 @@
22

33
import java.util.List;
44
import java.util.Optional;
5+
import java.util.Set;
56
import java.util.stream.Collectors;
7+
import java.util.LinkedHashSet;
68

79
import org.springframework.data.domain.Page;
810
import org.springframework.data.domain.Pageable;
@@ -39,6 +41,10 @@
3941
import com.gotogether.global.apipayload.code.status.ErrorStatus;
4042
import com.gotogether.global.apipayload.exception.GeneralException;
4143
import com.gotogether.global.common.service.S3UploadService;
44+
import com.gotogether.global.util.ExcelGenerator;
45+
import com.gotogether.domain.ticketoptionassignment.entity.TicketOptionAssignment;
46+
import com.gotogether.domain.ticketoptionassignment.repository.TicketOptionAssignmentRepository;
47+
import com.gotogether.domain.ticketoptionanswer.repository.TicketOptionAnswerRepository;
4248

4349
import lombok.RequiredArgsConstructor;
4450

@@ -56,6 +62,8 @@ public class HostChannelServiceImpl implements HostChannelService {
5662
private final EventFacade eventFacade;
5763
private final TicketQrCodeService ticketQrCodeService;
5864
private final S3UploadService s3UploadService;
65+
private final TicketOptionAssignmentRepository ticketOptionAssignmentRepository;
66+
private final TicketOptionAnswerRepository ticketOptionAnswerRepository;
5967

6068
@Override
6169
@Transactional
@@ -224,6 +232,37 @@ public void approveOrderStatus(Long orderId) {
224232
order.approveOrder();
225233
}
226234

235+
@Override
236+
@Transactional(readOnly = true)
237+
public byte[] generateParticipantManagementExcel(Long eventId) {
238+
List<Ticket> tickets = ticketRepository.findByEventId(eventId);
239+
List<Order> orders = orderRepository.findCompletedOrdersByEventId(eventId, OrderStatus.COMPLETED);
240+
241+
Set<String> optionNames = extractOptionNames(tickets);
242+
243+
try {
244+
return ExcelGenerator.generateParticipantExcel(orders, optionNames,
245+
orderId -> ticketOptionAnswerRepository.findByOrderId(orderId));
246+
} catch (RuntimeException e) {
247+
throw new GeneralException(ErrorStatus._EXCEL_GENERATION_FAILED);
248+
}
249+
}
250+
251+
@Override
252+
@Transactional(readOnly = true)
253+
public String generateParticipantManagementExcelFileName(Long eventId) {
254+
Event event = eventRepository.findById(eventId)
255+
.orElseThrow(() -> new GeneralException(ErrorStatus._EVENT_NOT_FOUND));
256+
257+
String eventName = event.getTitle()
258+
.replaceAll("[\\/:*?\"<>|]", "")
259+
.replaceAll("\\s+", "_");
260+
261+
String currentDate = java.time.LocalDate.now().toString();
262+
263+
return String.format("%s_구매참가자목록_%s.xlsx", eventName, currentDate);
264+
}
265+
227266
private User getUser(Long userId) {
228267
return userRepository.findById(userId)
229268
.orElseThrow(() -> new GeneralException(ErrorStatus._USER_NOT_FOUND));
@@ -274,4 +313,23 @@ private void updateProfileImageToFinal(HostChannel hostChannel, String imageUrl)
274313
String finalUrl = s3UploadService.moveTempImageToFinal(imageUrl);
275314
hostChannel.updateProfileImageUrl(finalUrl);
276315
}
316+
317+
private Set<String> extractOptionNames(List<Ticket> tickets) {
318+
Set<String> optionNames = new LinkedHashSet<>();
319+
320+
for (Ticket ticket : tickets) {
321+
List<TicketOptionAssignment> assignments = ticketOptionAssignmentRepository.findAllByTicket(ticket);
322+
323+
for (TicketOptionAssignment assignment : assignments) {
324+
if (assignment != null && assignment.getTicketOption() != null) {
325+
String optionName = assignment.getTicketOption().getName();
326+
if (optionName != null && !optionName.trim().isEmpty()) {
327+
optionNames.add(optionName);
328+
}
329+
}
330+
}
331+
}
332+
333+
return optionNames;
334+
}
277335
}

src/main/java/com/gotogether/domain/ticketoptionanswer/repository/TicketOptionAnswerRepository.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,4 +30,6 @@ WHERE e.ticketOption.id IN (
3030
List<TicketOptionAnswer> findByTicketId(@Param("ticketId") Long ticketId);
3131

3232
boolean existsByTicketOption(TicketOption ticketOption);
33+
34+
List<TicketOptionAnswer> findByOrderId(Long orderId);
3335
}

src/main/java/com/gotogether/global/apipayload/code/status/ErrorStatus.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,9 @@ public enum ErrorStatus implements BaseErrorCode {
6666
// 미사용
6767
_TICKET_OPTION_ANSWER_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "TICKET_OPTION_ANSWER4002", "해당 티켓 옵션에 대한 응답이 이미 존재합니다."),
6868

69-
_TERM_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "TERM4001", "이미 약관에 동의한 사용자입니다.");
69+
_TERM_ALREADY_EXISTS(HttpStatus.BAD_REQUEST, "TERM4001", "이미 약관에 동의한 사용자입니다."),
70+
71+
_EXCEL_GENERATION_FAILED(HttpStatus.INTERNAL_SERVER_ERROR, "EXCEL5001", "엑셀 파일 생성에 실패했습니다.");
7072

7173
private final HttpStatus httpStatus;
7274
private final String code;
Lines changed: 104 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,104 @@
1+
package com.gotogether.global.util;
2+
3+
import java.io.ByteArrayOutputStream;
4+
import java.io.IOException;
5+
import java.util.List;
6+
import java.util.Map;
7+
import java.util.Set;
8+
9+
import org.apache.poi.ss.usermodel.Row;
10+
import org.apache.poi.ss.usermodel.Sheet;
11+
import org.apache.poi.ss.usermodel.Workbook;
12+
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
13+
14+
import com.gotogether.domain.order.entity.Order;
15+
import com.gotogether.domain.ticketoptionanswer.entity.TicketOptionAnswer;
16+
17+
import lombok.AccessLevel;
18+
import lombok.NoArgsConstructor;
19+
20+
@NoArgsConstructor(access = AccessLevel.PRIVATE)
21+
public class ExcelGenerator {
22+
23+
private static final String SHEET_NAME = "구매참가자목록";
24+
private static final String[] HEADERS = {"이름", "이메일", "휴대폰 번호", "구매 일자", "티켓 이름"};
25+
26+
public static byte[] generateParticipantExcel(List<Order> orders, Set<String> optionNames,
27+
java.util.function.Function<Long, List<TicketOptionAnswer>> answerProvider) {
28+
try (Workbook workbook = new XSSFWorkbook();
29+
ByteArrayOutputStream out = new ByteArrayOutputStream()) {
30+
31+
Sheet sheet = workbook.createSheet(SHEET_NAME);
32+
33+
createHeaderRow(sheet, optionNames);
34+
createDataRows(sheet, orders, optionNames, answerProvider);
35+
36+
workbook.write(out);
37+
return out.toByteArray();
38+
} catch (IOException e) {
39+
throw new RuntimeException("엑셀 파일 생성 중 오류 발생: ", e);
40+
}
41+
}
42+
43+
private static void createHeaderRow(Sheet sheet, Set<String> optionNames) {
44+
Row headerRow = sheet.createRow(0);
45+
int colIdx = 0;
46+
47+
for (String header : HEADERS) {
48+
headerRow.createCell(colIdx++).setCellValue(header);
49+
}
50+
51+
for (String optionName : optionNames) {
52+
headerRow.createCell(colIdx++).setCellValue(optionName);
53+
}
54+
}
55+
56+
private static void createDataRows(Sheet sheet, List<Order> orders, Set<String> optionNames,
57+
java.util.function.Function<Long, List<TicketOptionAnswer>> answerProvider) {
58+
int rowIdx = 1;
59+
60+
for (Order order : orders) {
61+
Row row = sheet.createRow(rowIdx++);
62+
int cellIdx = 0;
63+
64+
row.createCell(cellIdx++).setCellValue(order.getUser().getName());
65+
row.createCell(cellIdx++).setCellValue(order.getUser().getEmail());
66+
row.createCell(cellIdx++).setCellValue(order.getUser().getPhoneNumber());
67+
row.createCell(cellIdx++).setCellValue(order.getCreatedAt().toLocalDate().toString());
68+
row.createCell(cellIdx++).setCellValue(order.getTicket().getName());
69+
70+
Map<String, String> optionAnswerMap = createOptionAnswerMap(order, answerProvider);
71+
for (String optionName : optionNames) {
72+
row.createCell(cellIdx++).setCellValue(optionAnswerMap.getOrDefault(optionName, ""));
73+
}
74+
}
75+
}
76+
77+
private static Map<String, String> createOptionAnswerMap(Order order,
78+
java.util.function.Function<Long, List<TicketOptionAnswer>> answerProvider) {
79+
Map<String, String> optionAnswerMap = new java.util.HashMap<>();
80+
81+
List<TicketOptionAnswer> answers = answerProvider.apply(order.getId());
82+
for (TicketOptionAnswer answer : answers) {
83+
String optionName = answer.getTicketOption().getName();
84+
String value = getAnswerValue(answer);
85+
86+
if (optionAnswerMap.containsKey(optionName)) {
87+
value = optionAnswerMap.get(optionName) + ", " + value;
88+
}
89+
optionAnswerMap.put(optionName, value);
90+
}
91+
92+
return optionAnswerMap;
93+
}
94+
95+
private static String getAnswerValue(TicketOptionAnswer answer) {
96+
if (answer.getAnswerText() != null) {
97+
return answer.getAnswerText();
98+
}
99+
if (answer.getTicketOptionChoice() != null) {
100+
return answer.getTicketOptionChoice().getName();
101+
}
102+
return "";
103+
}
104+
}

0 commit comments

Comments
 (0)