Skip to content

Commit 28230a0

Browse files
authored
배포 : 기능개선
* fix: SSE 모듈 TimeZone 설정 추가 * fix: 티켓팅 유저 참여 상태 변경 캐시 반영하도록 수정 * refactor : 이벤트 리스트 반환 값 변경, 경품수령 처리시 이미지 받도록 변경 * refactor : 경품 수령처리시 이미지 URL도 함께 변경 * refactor : 엑셀에 서명 이미지 추가 * refactor : of -> from 으로 변경 - 완성된 DTO를 단순 생성하는 것이 아니라, 엔티티에서 DTO로 변환하는 로직이므로 의미에 맞게 메서드명을 변경함 * test : 서명상태 변경시 이미지도 함께 업로드 함에 따라 테스트 코드 변경 * refactor : 서명상태 변경시 이미지 받도록 controller 변경 * refactor : 이벤트 리스트 반환시 수령 대기 인원 함께 반환하도록 변경 * fix: 티켓팅 유저가 취소 이후에도 재참여 가능하도록 로직 변경
1 parent e384909 commit 28230a0

13 files changed

Lines changed: 162 additions & 42 deletions

File tree

codin-ticketing-sse/src/main/java/inu/codin/codinticketingsse/CodinTicketingSseApplication.java

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,14 @@
55
import org.springframework.boot.autoconfigure.SpringBootApplication;
66
import org.springframework.scheduling.annotation.EnableScheduling;
77

8+
import java.util.TimeZone;
9+
810
@SpringBootApplication
911
@EnableScheduling
1012
public class CodinTicketingSseApplication {
1113

1214
public static void main(String[] args) {
15+
TimeZone.setDefault(TimeZone.getTimeZone("Asia/Seoul"));
1316
SpringApplication.run(CodinTicketingSseApplication.class, args);
1417
}
1518

src/main/java/inu/codin/codinticketingapi/domain/admin/controller/EventAdminControllerImpl.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ public ResponseEntity<SingleResponse<EventResponse>> createEvent(
4646
public ResponseEntity<SingleResponse<EventPageResponse>> getEventListByManager(@RequestParam String status, @RequestParam("page") @NotNull int pageNumber) {
4747

4848
return ResponseEntity.ok(new SingleResponse<>(200, "[티켓팅 관리자] 이벤트 게시물 리스트 반환 성공",
49-
eventAdminService.getEventListByManager(status, pageNumber)));
49+
eventAdminService.eventPageResponseWithStatus(status, pageNumber)));
5050
}
5151

5252
/**
@@ -112,9 +112,9 @@ public ResponseEntity<SingleResponse<EventParticipationProfilePageResponse>> get
112112
*/
113113
@PreAuthorize("hasAnyRole('MANAGER', 'ADMIN')")
114114
@PutMapping("{eventId}/management/status/{userId}")
115-
public ResponseEntity<SingleResponse<Boolean>> changeReceiveStatus(@PathVariable Long eventId, @PathVariable String userId) {
115+
public ResponseEntity<SingleResponse<Boolean>> changeReceiveStatus(@PathVariable Long eventId, @PathVariable String userId, @RequestPart(value = "signImage", required = false) MultipartFile eventImage) {
116116

117-
return ResponseEntity.ok(new SingleResponse<>(200, "수령완료 변경 성공", eventAdminService.changeReceiveStatus(eventId, userId)));
117+
return ResponseEntity.ok(new SingleResponse<>(200, "수령완료 변경 성공", eventAdminService.changeReceiveStatus(eventId, userId, eventImage)));
118118
}
119119

120120
/**

src/main/java/inu/codin/codinticketingapi/domain/admin/controller/swagger/EventAdminController.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -92,7 +92,8 @@ ResponseEntity<SingleResponse<EventParticipationProfilePageResponse>> getEventPa
9292
})
9393
ResponseEntity<SingleResponse<Boolean>> changeReceiveStatus(
9494
@Parameter(description = "이벤트 ID", example = "1", required = true) @PathVariable Long eventId,
95-
@Parameter(description = "수령 상태를 변경할 사용자 ID", example = "user123", required = true) @PathVariable String userId);
95+
@Parameter(description = "수령 상태를 변경할 사용자 ID", example = "user123", required = true) @PathVariable String userId,
96+
@Parameter(description = "서명 이미지", required = true) @RequestPart(value = "eventImage", required = false) MultipartFile eventImage);
9697

9798

9899
@Operation(summary = "이벤트 잔여 수량 조회", description = "지정된 이벤트의 티켓/상품 잔여 수량을 조회합니다. 관리자/매니저 권한이 필요합니다.")

src/main/java/inu/codin/codinticketingapi/domain/admin/service/EventAdminService.java

Lines changed: 35 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,10 @@
3232
import org.springframework.web.multipart.MultipartFile;
3333

3434
import java.time.LocalDateTime;
35+
import java.util.Collections;
3536
import java.util.List;
37+
import java.util.Map;
38+
import java.util.stream.Collectors;
3639

3740
@Service
3841
@RequiredArgsConstructor
@@ -65,10 +68,12 @@ public EventResponse createEvent(EventCreateRequest request, MultipartFile event
6568
return EventResponse.of(savedEvent);
6669
}
6770

68-
public EventPageResponse getEventListByManager(String status, int pageNumber) {
69-
findAdminUser();
71+
public EventPageResponse eventPageResponseWithStatus(String status, int pageNumber) {
72+
Pageable pageable = PageRequest.of(pageNumber - 1, PAGE_SIZE, Sort.by("createdAt").descending());
73+
Page<Event> eventPage = findEventsByStatus(status, pageable);
74+
Map<Long, Long> waitingCountMap = getWaitingCountMap(eventPage);
7075

71-
return eventPageResponseWithStatus(status, pageNumber - 1);
76+
return EventPageResponse.from(eventPage, waitingCountMap);
7277
}
7378

7479
@Transactional
@@ -141,9 +146,11 @@ public EventParticipationProfilePageResponse getParticipationList(Long eventId,
141146
}
142147

143148
@Transactional
144-
public boolean changeReceiveStatus(Long eventId, String userId) {
149+
public boolean changeReceiveStatus(Long eventId, String userId, MultipartFile image) {
145150
Participation findParticipation = getParticipationByEventIdAndUserId(eventId, userId);
146-
findParticipation.changeStatusCompleted();
151+
String imageURL = imageService.handleImageUpload(image);
152+
153+
findParticipation.changeStatusCompleted(imageURL);
147154

148155
return true;
149156
}
@@ -179,17 +186,12 @@ public Boolean openEvent(Long eventId) {
179186
return true;
180187
}
181188

182-
private EventPageResponse eventPageResponseWithStatus(String status, int pageNumber) {
183-
Pageable pageable = PageRequest.of(pageNumber, PAGE_SIZE, Sort.by("createdAt").descending());
184-
189+
private Page<Event> findEventsByStatus(String status, Pageable pageable) {
185190
return switch (status) {
186-
case "all" -> EventPageResponse.of(eventRepository.findAll(pageable));
187-
case "upcoming" ->
188-
EventPageResponse.of(eventRepository.findAllByEventStatusAndDeletedAtIsNull(EventStatus.UPCOMING, pageable));
189-
case "open" ->
190-
EventPageResponse.of(eventRepository.findAllByEventStatusAndDeletedAtIsNull(EventStatus.ACTIVE, pageable));
191-
case "ended" ->
192-
EventPageResponse.of(eventRepository.findAllByEventStatusAndDeletedAtIsNull(EventStatus.ENDED, pageable));
191+
case "all" -> eventRepository.findAll(pageable);
192+
case "upcoming" -> eventRepository.findAllByEventStatusAndDeletedAtIsNull(EventStatus.UPCOMING, pageable);
193+
case "open" -> eventRepository.findAllByEventStatusAndDeletedAtIsNull(EventStatus.ACTIVE, pageable);
194+
case "ended" -> eventRepository.findAllByEventStatusAndDeletedAtIsNull(EventStatus.ENDED, pageable);
193195
default -> throw new TicketingException(TicketingErrorCode.EVENT_NOT_FOUND);
194196
};
195197
}
@@ -239,4 +241,22 @@ private Participation getParticipationByEventIdAndUserId(Long eventId, String us
239241

240242
return participationRepository.findByEvent_IdAndUserId(eventId, userId).orElseThrow(() -> new UserException(UserErrorCode.USER_VALIDATION_FAILED));
241243
}
244+
245+
private Map<Long, Long> getWaitingCountMap(Page<Event> eventPage) {
246+
List<Long> eventIds = eventPage.stream()
247+
.map(Event::getId)
248+
.toList();
249+
250+
if (eventIds.isEmpty()) {
251+
return Collections.emptyMap();
252+
}
253+
254+
return participationRepository
255+
.countWaitingByEventIds(ParticipationStatus.WAITING, eventIds)
256+
.stream()
257+
.collect(Collectors.toMap(
258+
row -> (Long) row[0],
259+
row -> (Long) row[1]
260+
));
261+
}
242262
}

src/main/java/inu/codin/codinticketingapi/domain/admin/service/ExcelService.java

Lines changed: 49 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,23 @@
1010
import inu.codin.codinticketingapi.domain.ticketing.repository.EventRepository;
1111
import inu.codin.codinticketingapi.domain.ticketing.repository.ParticipationRepository;
1212
import lombok.RequiredArgsConstructor;
13-
import org.apache.poi.ss.usermodel.Cell;
14-
import org.apache.poi.ss.usermodel.Row;
15-
import org.apache.poi.ss.usermodel.Sheet;
16-
import org.apache.poi.ss.usermodel.Workbook;
13+
import lombok.extern.slf4j.Slf4j;
14+
import org.apache.poi.ss.usermodel.*;
15+
import org.apache.poi.util.Units;
1716
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
17+
import org.springframework.data.domain.Page;
1818
import org.springframework.stereotype.Service;
1919
import org.springframework.transaction.annotation.Transactional;
2020

2121
import java.io.ByteArrayOutputStream;
2222
import java.io.IOException;
23+
import java.io.InputStream;
24+
import java.net.URL;
2325
import java.net.URLEncoder;
2426
import java.nio.charset.StandardCharsets;
2527
import java.util.List;
2628

29+
@Slf4j
2730
@Service
2831
@RequiredArgsConstructor
2932
public class ExcelService {
@@ -35,6 +38,8 @@ public class ExcelService {
3538
private final static String SHEET_NAME_SUFFIX = "_참가자";
3639
private final static String UNKNOWN = "UNKNOWN";
3740
private final static String[] HEADERS = {"사용자 ID", "이름", "학과", "학번", "경품 수령 상태", "교환권 번호", "서명"};
41+
private final static int SIGN_NUM = 6;
42+
private final static int PADDING = 5 * Units.EMU_PER_PIXEL;
3843

3944
@Transactional(readOnly = true)
4045
public ExcelResponse getExcel(Long eventId) {
@@ -94,12 +99,13 @@ private void createHeaderRow(Sheet sheet) {
9499

95100
private List<Participation> getParticipation(Long eventId) {
96101

97-
98102
return participationRepository.findAllByEvent_Id(eventId);
99103
}
100104

101105
private void populateDataRows(Sheet sheet, List<Participation> participationList) {
102106
int rowNum = ROW_START;
107+
Workbook workbook = sheet.getWorkbook();
108+
Drawing<?> drawing = sheet.createDrawingPatriarch();
103109

104110
if (participationList.isEmpty()) {
105111
Row row = sheet.createRow(rowNum);
@@ -110,17 +116,55 @@ private void populateDataRows(Sheet sheet, List<Participation> participationList
110116

111117
for (Participation participation : participationList) {
112118
Row row = sheet.createRow(rowNum++);
119+
row.setHeightInPoints(70);
120+
113121
row.createCell(0).setCellValue(participation.getUserId());
114122
row.createCell(1).setCellValue(participation.getName());
115123
row.createCell(2).setCellValue(participation.getDepartment() != null ? participation.getDepartment().name() : UNKNOWN);
116124
row.createCell(3).setCellValue(participation.getStudentId());
117125
row.createCell(4).setCellValue(participation.getStatus() != null ? participation.getStatus().name() : UNKNOWN);
118126
row.createCell(5).setCellValue(participation.getTicketNumber());
127+
setImage(workbook, drawing, row, participation);
128+
}
129+
}
130+
131+
private void setImage(Workbook workbook, Drawing<?> drawing, Row row, Participation participation) {
132+
String imageURL = participation.getSignatureImgUrl();
133+
134+
if (imageURL != null && !imageURL.isBlank()) {
135+
try (InputStream is = new URL(imageURL).openStream()) {
136+
byte[] bytes = is.readAllBytes();
137+
int pictureIdx = workbook.addPicture(bytes, Workbook.PICTURE_TYPE_PNG);
138+
139+
CreationHelper helper = workbook.getCreationHelper();
140+
ClientAnchor anchor = helper.createClientAnchor();
141+
142+
anchor.setCol1(SIGN_NUM);
143+
anchor.setRow1(row.getRowNum());
144+
anchor.setCol2(SIGN_NUM + 1);
145+
anchor.setRow2(row.getRowNum() + 1);
146+
147+
anchor.setDx1(PADDING);
148+
anchor.setDy1(PADDING);
149+
anchor.setDx2(-PADDING);
150+
anchor.setDy2(-PADDING);
151+
152+
drawing.createPicture(anchor, pictureIdx);
153+
} catch (Exception e) {
154+
row.createCell(6).setCellValue("이미지 로드 실패");
155+
156+
log.error("이미지 로드 실패 URL: {}", imageURL, e);
157+
}
119158
}
120159
}
121160

122161
private void autoSizeAllColumns(Sheet sheet) {
123162
for (int i = 0; i < HEADERS.length; i++) {
163+
if (i == SIGN_NUM) {
164+
sheet.setColumnWidth(i, 25 * 256);
165+
166+
continue;
167+
}
124168
sheet.autoSizeColumn(i);
125169
}
126170
}

src/main/java/inu/codin/codinticketingapi/domain/ticketing/dto/response/EventPageDetailResponse.java

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@ public class EventPageDetailResponse {
4646
@Schema(description = "이벤트 현재 수량", example = "80")
4747
private int currentQuantity;
4848

49+
@Schema(description = "이벤트 수령 대기 수량", example = "80")
50+
private int waitQuantity;
51+
4952
@Schema(description = "이벤트 상태 enum(UPCOMING, ACTIVE, ENDED)", example = "UPCOMING")
5053
private EventStatus eventStatus;
5154

@@ -62,4 +65,19 @@ public static EventPageDetailResponse of(Event event) {
6265
.eventStatus(event.getEventStatus())
6366
.build();
6467
}
68+
69+
public static EventPageDetailResponse of(Event event, int waitQuantity) {
70+
return EventPageDetailResponse.builder()
71+
.eventId(event.getId())
72+
.eventTitle(event.getTitle())
73+
.eventImageUrl(event.getEventImageUrl())
74+
.eventTime(event.getEventTime())
75+
.eventEndTime(event.getEventEndTime())
76+
.locationInfo(event.getLocationInfo())
77+
.quantity(event.getStock().getInitialStock())
78+
.currentQuantity(event.getStock().getStock())
79+
.waitQuantity(waitQuantity)
80+
.eventStatus(event.getEventStatus())
81+
.build();
82+
}
6583
}

src/main/java/inu/codin/codinticketingapi/domain/ticketing/dto/response/EventPageResponse.java

Lines changed: 20 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@
1010

1111
import java.util.ArrayList;
1212
import java.util.List;
13+
import java.util.Map;
1314

1415
@Getter
1516
@Builder
@@ -36,15 +37,30 @@ public static EventPageResponse of(List<EventPageDetailResponse> eventList, long
3637
return new EventPageResponse(eventList, lastPage, nextPage);
3738
}
3839

39-
public static EventPageResponse of(Page<Event> page) {
40-
List<EventPageDetailResponse> eventList = page.getContent().stream()
40+
public static EventPageResponse from(Page<Event> eventPage) {
41+
List<EventPageDetailResponse> eventList = eventPage.getContent().stream()
4142
.map(EventPageDetailResponse::of)
4243
.toList();
4344

4445
return new EventPageResponse(
4546
eventList,
46-
page.getTotalPages() - 1, // 0-based index이므로 -1
47-
page.hasNext() ? page.getNumber() + 1 : -1 // 다음 페이지가 없으면 -1
47+
eventPage.getTotalPages() - 1,
48+
eventPage.hasNext() ? eventPage.getNumber() + 1 : -1
49+
);
50+
}
51+
52+
public static EventPageResponse from(Page<Event> eventPage, Map<Long, Long> waitingCountMap) {
53+
List<EventPageDetailResponse> eventList = eventPage.getContent().stream()
54+
.map(event -> EventPageDetailResponse.of(
55+
event,
56+
waitingCountMap.getOrDefault(event.getId(), 0L).intValue()
57+
))
58+
.toList();
59+
60+
return new EventPageResponse(
61+
eventList,
62+
eventPage.getTotalPages() - 1, // 0-based index이므로 -1
63+
eventPage.hasNext() ? eventPage.getNumber() + 1 : -1 // 다음 페이지가 없으면 -1
4864
);
4965
}
5066
}

src/main/java/inu/codin/codinticketingapi/domain/ticketing/entity/Participation.java

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,6 @@ public class Participation extends BaseEntity {
4646
@Column(name = "ticket_number", nullable = false)
4747
private Integer ticketNumber;
4848

49-
@Setter
5049
@Column(name = "signature_img_url")
5150
private String signatureImgUrl;
5251

@@ -61,8 +60,9 @@ public Participation(Event event, Integer ticketNumber, UserInfoResponse userInf
6160
}
6261

6362
/** 경품 수령 처리 */
64-
public void changeStatusCompleted() {
63+
public void changeStatusCompleted(String imageUrl) {
6564
this.status = ParticipationStatus.COMPLETED;
65+
this.signatureImgUrl = imageUrl;
6666
}
6767

6868
/** 취소 처리 */

src/main/java/inu/codin/codinticketingapi/domain/ticketing/repository/ParticipationRepository.java

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,4 +83,16 @@ Page<EventParticipationHistoryDto> findHistoryByUserIdAndCanceled(
8383
WHERE p.userId = :userId AND p.event = :event
8484
""")
8585
Optional<Participation> findByUserIdAndEvent(String userId, Event event);
86+
87+
@Query("""
88+
SELECT p.event.id, COUNT(p)
89+
FROM Participation p
90+
WHERE p.status = :status
91+
AND p.event.id IN :eventIds
92+
GROUP BY p.event.id
93+
""")
94+
List<Object[]> countWaitingByEventIds(
95+
@Param("status") ParticipationStatus status,
96+
@Param("eventIds") List<Long> eventIds
97+
);
8698
}

src/main/java/inu/codin/codinticketingapi/domain/ticketing/service/EventService.java

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -11,10 +11,7 @@
1111
import inu.codin.codinticketingapi.domain.ticketing.repository.EventRepository;
1212
import inu.codin.codinticketingapi.domain.ticketing.repository.ParticipationRepository;
1313
import inu.codin.codinticketingapi.domain.user.dto.UserInfoResponse;
14-
import inu.codin.codinticketingapi.domain.user.exception.UserErrorCode;
15-
import inu.codin.codinticketingapi.domain.user.exception.UserException;
1614
import inu.codin.codinticketingapi.domain.user.service.UserClientService;
17-
import jakarta.validation.Valid;
1815
import jakarta.validation.constraints.NotNull;
1916
import jakarta.validation.constraints.PositiveOrZero;
2017
import lombok.RequiredArgsConstructor;
@@ -36,7 +33,7 @@ public class EventService {
3633
@Transactional(readOnly = true)
3734
public EventPageResponse getEventList(@NotNull Campus campus, @PositiveOrZero int pageNumber) {
3835
Pageable pageable = PageRequest.of(pageNumber, 10, Sort.by("createdAt").descending());
39-
return EventPageResponse.of(eventRepository.findByCampus(campus, pageable));
36+
return EventPageResponse.from(eventRepository.findByCampus(campus, pageable));
4037
}
4138

4239
@Transactional(readOnly = true)

0 commit comments

Comments
 (0)