Skip to content

Commit 3147226

Browse files
authored
Merge pull request #46 from CodIN-INU/develop
refactor : 티켓 번호 발부를 위한 구현, 티켓팅 재참여 가능
2 parents 28230a0 + a9f2ad5 commit 3147226

18 files changed

Lines changed: 485 additions & 114 deletions

File tree

build.gradle

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,8 @@ dependencies {
6161
// Excel
6262
implementation 'org.apache.poi:poi:5.4.1'
6363
implementation 'org.apache.poi:poi-ooxml:5.4.1'
64+
// serializer
65+
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
6466

6567
compileOnly 'org.projectlombok:lombok'
6668
annotationProcessor 'org.projectlombok:lombok'
Lines changed: 75 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,99 @@
11
package inu.codin.codinticketingsse.common.exception;
22

33
import inu.codin.codinticketingsse.common.response.ExceptionResponse;
4+
import inu.codin.codinticketingsse.security.exception.SecurityErrorCode;
5+
import inu.codin.codinticketingsse.security.exception.SecurityException;
6+
import inu.codin.codinticketingsse.sse.exception.SseErrorCode;
7+
import inu.codin.codinticketingsse.sse.exception.SseException;
8+
import jakarta.validation.ConstraintViolationException;
49
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.core.convert.ConversionFailedException;
511
import org.springframework.http.HttpStatus;
6-
import org.springframework.http.MediaType;
712
import org.springframework.http.ResponseEntity;
13+
import org.springframework.security.access.AccessDeniedException;
14+
import org.springframework.web.bind.MethodArgumentNotValidException;
815
import org.springframework.web.bind.annotation.ControllerAdvice;
916
import org.springframework.web.bind.annotation.ExceptionHandler;
17+
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
18+
import org.springframework.web.multipart.support.MissingServletRequestPartException;
1019

1120
@ControllerAdvice
1221
@Slf4j
1322
public class GlobalExceptionHandler {
1423

1524
@ExceptionHandler(Exception.class)
1625
protected ResponseEntity<ExceptionResponse> handleException(Exception e) {
17-
return ResponseEntity.status(HttpStatus.NOT_FOUND)
18-
.contentType(MediaType.APPLICATION_JSON)
19-
.body(new ExceptionResponse( e.getMessage(), HttpStatus.NOT_FOUND.value()));
26+
log.warn("[Exception] Class: {}, Error Message : {}, Stack Trace: {}",
27+
e.getClass().getSimpleName(),
28+
e.getMessage(),
29+
e.getStackTrace()[0].toString());
30+
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
31+
.body(new ExceptionResponse(e.getMessage(), HttpStatus.INTERNAL_SERVER_ERROR.value()));
2032
}
2133

2234
@ExceptionHandler(GlobalException.class)
2335
protected ResponseEntity<ExceptionResponse> handleGlobalException(GlobalException e) {
36+
log.warn("[GlobalException] Class: {}, Error Message : {}", e.getClass().getSimpleName(), e.getMessage());
2437
GlobalErrorCode code = e.getErrorCode();
2538
return ResponseEntity.status(code.httpStatus())
2639
.body(new ExceptionResponse(e.getMessage(), code.httpStatus().value()));
2740
}
41+
42+
@ExceptionHandler(SseException.class)
43+
public ResponseEntity<ExceptionResponse> handleSseException(SseException e) {
44+
log.warn("[SseException] Class: {}, Error Message : {}", e.getClass().getSimpleName(), e.getMessage());
45+
SseErrorCode code = e.getErrorCode();
46+
return ResponseEntity.status(code.httpStatus())
47+
.body(new ExceptionResponse(e.getMessage(), code.httpStatus().value()));
48+
}
49+
50+
@ExceptionHandler(SecurityException.class)
51+
public ResponseEntity<ExceptionResponse> handleSecurityException(SecurityException e) {
52+
log.warn("[SecurityException] Class: {}, Error Message : {}", e.getClass().getSimpleName(), e.getMessage());
53+
SecurityErrorCode code = e.getSecurityErrorCode();
54+
return ResponseEntity.status(code.httpStatus())
55+
.body(new ExceptionResponse(e.getMessage(), code.httpStatus().value()));
56+
}
57+
58+
@ExceptionHandler(MethodArgumentNotValidException.class)
59+
public ResponseEntity<ExceptionResponse> handleValidationException(MethodArgumentNotValidException e) {
60+
log.warn("[MethodArgumentNotValidException] Error Message : {}", e.getMessage());
61+
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
62+
.body(new ExceptionResponse(e.getMessage(), HttpStatus.BAD_REQUEST.value()));
63+
}
64+
65+
@ExceptionHandler(ConstraintViolationException.class)
66+
public ResponseEntity<ExceptionResponse> handleConstraintViolationException(ConstraintViolationException e) {
67+
log.warn("[ConstraintViolationException] Error Message : {}", e.getMessage());
68+
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
69+
.body(new ExceptionResponse(e.getMessage(), HttpStatus.BAD_REQUEST.value()));
70+
}
71+
72+
@ExceptionHandler(AccessDeniedException.class)
73+
public ResponseEntity<ExceptionResponse> handleAccessDeniedException(AccessDeniedException e) {
74+
log.warn("[AccessDeniedException] Error Message : {}", e.getMessage());
75+
return ResponseEntity.status(HttpStatus.FORBIDDEN)
76+
.body(new ExceptionResponse(e.getMessage(), HttpStatus.FORBIDDEN.value()));
77+
}
78+
79+
@ExceptionHandler(MissingServletRequestPartException.class)
80+
public ResponseEntity<ExceptionResponse> handleMissingServletRequestPartException(MissingServletRequestPartException e) {
81+
log.warn("[MissingServletRequestPartException] Error Message : {}", e.getMessage());
82+
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
83+
.body(new ExceptionResponse(e.getMessage(), HttpStatus.BAD_REQUEST.value()));
84+
}
85+
86+
@ExceptionHandler(MethodArgumentTypeMismatchException.class)
87+
public ResponseEntity<ExceptionResponse> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
88+
log.warn("[MethodArgumentTypeMismatchException] Error Message : {}", e.getMessage());
89+
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
90+
.body(new ExceptionResponse(e.getMessage(), HttpStatus.BAD_REQUEST.value()));
91+
}
92+
93+
@ExceptionHandler(ConversionFailedException.class)
94+
public ResponseEntity<ExceptionResponse> handleConversionFailedException(ConversionFailedException e) {
95+
log.warn("[ConversionFailedException] Error Message : {}", e.getMessage());
96+
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
97+
.body(new ExceptionResponse(e.getMessage(), HttpStatus.BAD_REQUEST.value()));
98+
}
2899
}

codin-ticketing-sse/src/main/java/inu/codin/codinticketingsse/config/SecurityConfig.java

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
import inu.codin.codinticketingsse.security.filter.TokenValidationFilter;
66
import inu.codin.codinticketingsse.security.jwt.JwtTokenValidator;
77
import lombok.RequiredArgsConstructor;
8+
import org.springframework.beans.factory.annotation.Value;
89
import org.springframework.context.annotation.Bean;
910
import org.springframework.context.annotation.Configuration;
1011
import org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
@@ -26,10 +27,12 @@
2627
@EnableMethodSecurity(prePostEnabled = true)
2728
@RequiredArgsConstructor
2829
public class SecurityConfig {
29-
3030
private final JwtTokenValidator jwtTokenValidator;
3131
private final CustomAccessDeniedHandler customAccessDeniedHandler;
3232

33+
@Value("${server.domain}")
34+
private String BASE_DOMAIN_URL;
35+
3336
@Bean
3437
public SecurityFilterChain filterChain(HttpSecurity http, CorsConfigurationSource corsConfigurationSource) throws Exception {
3538
return http
@@ -63,7 +66,7 @@ public CorsConfigurationSource corsConfigurationSource() {
6366
CorsConfiguration config = new CorsConfiguration();
6467

6568
config.setAllowCredentials(true);
66-
config.setAllowedOrigins(List.of("http://localhost:3000"));
69+
config.setAllowedOrigins(List.of("http://localhost:3000", BASE_DOMAIN_URL, "https://front-end-dun-mu.vercel.app"));
6770
config.setAllowedMethods(List.of("GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"));
6871
config.setAllowedHeaders(List.of("*"));
6972
config.setExposedHeaders(List.of("*"));

codin-ticketing-sse/src/main/java/inu/codin/codinticketingsse/security/exception/SecurityException.java

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66

77
@Getter
88
public class SecurityException extends GlobalException {
9+
910
private final SecurityErrorCode securityErrorCode;
1011

1112
public SecurityException(SecurityErrorCode errorCode) {

src/main/java/inu/codin/codinticketingapi/CodinTicketingApiApplication.java

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import org.springframework.boot.SpringApplication;
44
import org.springframework.boot.autoconfigure.SpringBootApplication;
55
import org.springframework.cloud.openfeign.EnableFeignClients;
6+
import org.springframework.scheduling.annotation.EnableScheduling;
67

78
import java.util.TimeZone;
89

910
@SpringBootApplication
1011
@EnableFeignClients
12+
@EnableScheduling
1113
public class CodinTicketingApiApplication {
1214

1315
public static void main(String[] args) {

src/main/java/inu/codin/codinticketingapi/config/RedisConfig.java

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,8 @@
11
package inu.codin.codinticketingapi.config;
22

3+
import com.fasterxml.jackson.databind.ObjectMapper;
4+
import com.fasterxml.jackson.databind.SerializationFeature;
5+
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
36
import lombok.RequiredArgsConstructor;
47
import lombok.extern.slf4j.Slf4j;
58
import org.springframework.beans.factory.annotation.Value;
@@ -49,12 +52,42 @@ public RedisConnectionFactory redisConnectionFactory() {
4952

5053
@Bean
5154
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
55+
ObjectMapper objectMapper = new ObjectMapper();
56+
objectMapper.registerModule(new JavaTimeModule());
57+
// LocalDateTime을 타임스탬프(숫자)가 아닌 ISO-8601 형식의 문자열로 직렬화
58+
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
59+
60+
// 2. ObjectMapper를 사용하는 Serializer 생성
61+
return getStringObjectRedisTemplate(redisConnectionFactory, objectMapper);
62+
}
63+
64+
@Bean
65+
public RedisTemplate<String, String> eventRedisTemplate (RedisConnectionFactory redisConnectionFactory) {
66+
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
67+
redisTemplate.setConnectionFactory(redisConnectionFactory);
68+
redisTemplate.setKeySerializer(new StringRedisSerializer());
69+
redisTemplate.setValueSerializer(new StringRedisSerializer());
70+
71+
return redisTemplate;
72+
}
73+
74+
@Bean
75+
public RedisTemplate<String, String> pingRedisTemplate (RedisConnectionFactory redisConnectionFactory) {
76+
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
77+
redisTemplate.setConnectionFactory(redisConnectionFactory);
78+
redisTemplate.setKeySerializer(new StringRedisSerializer());
79+
redisTemplate.setValueSerializer(new StringRedisSerializer());
80+
81+
return redisTemplate;
82+
}
83+
84+
private RedisTemplate<String, Object> getStringObjectRedisTemplate(RedisConnectionFactory redisConnectionFactory, ObjectMapper objectMapper) {
85+
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer(objectMapper);
5286
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
5387
redisTemplate.setConnectionFactory(redisConnectionFactory);
5488
redisTemplate.setKeySerializer(new StringRedisSerializer());
5589
redisTemplate.setDefaultSerializer(RedisSerializer.string());
56-
redisTemplate.setEnableTransactionSupport(true);
57-
redisTemplate.setValueSerializer(new GenericJackson2JsonRedisSerializer());
90+
redisTemplate.setValueSerializer(serializer);
5891
return redisTemplate;
5992
}
6093
}
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package inu.codin.codinticketingapi.config;
2+
3+
import org.springframework.context.annotation.Configuration;
4+
import org.springframework.scheduling.annotation.SchedulingConfigurer;
5+
import org.springframework.scheduling.concurrent.ThreadPoolTaskScheduler;
6+
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
7+
8+
@Configuration
9+
public class SchedulerConfig implements SchedulingConfigurer {
10+
private static final int POOL_SIZE = 10;
11+
12+
@Override
13+
public void configureTasks(ScheduledTaskRegistrar taskRegistrar) {
14+
ThreadPoolTaskScheduler scheduler = new ThreadPoolTaskScheduler();
15+
16+
// 스케줄러가 사용할 스레드 풀의 개수를 설정
17+
scheduler.setPoolSize(POOL_SIZE);
18+
19+
// 스레드 이름 접두사 설정
20+
scheduler.setThreadNamePrefix("scheduled-task-");
21+
22+
// 스케줄러 초기화
23+
scheduler.initialize();
24+
25+
// 생성한 스케줄러를 등록
26+
taskRegistrar.setTaskScheduler(scheduler);
27+
}
28+
}

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -93,7 +93,7 @@ ResponseEntity<SingleResponse<EventParticipationProfilePageResponse>> getEventPa
9393
ResponseEntity<SingleResponse<Boolean>> changeReceiveStatus(
9494
@Parameter(description = "이벤트 ID", example = "1", required = true) @PathVariable Long eventId,
9595
@Parameter(description = "수령 상태를 변경할 사용자 ID", example = "user123", required = true) @PathVariable String userId,
96-
@Parameter(description = "서명 이미지", required = true) @RequestPart(value = "eventImage", required = false) MultipartFile eventImage);
96+
@RequestPart(value = "eventImage", required = false) @Parameter(description = "서명 이미지", required = true, content = @Content(mediaType = MediaType.IMAGE_JPEG_VALUE)) MultipartFile eventImage);
9797

9898

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

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

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
import inu.codin.codinticketingapi.domain.ticketing.entity.Stock;
1717
import inu.codin.codinticketingapi.domain.ticketing.exception.TicketingErrorCode;
1818
import inu.codin.codinticketingapi.domain.ticketing.exception.TicketingException;
19+
import inu.codin.codinticketingapi.domain.ticketing.redis.RedisEventService;
1920
import inu.codin.codinticketingapi.domain.ticketing.repository.EventRepository;
2021
import inu.codin.codinticketingapi.domain.ticketing.repository.ParticipationRepository;
2122
import inu.codin.codinticketingapi.domain.user.exception.UserErrorCode;
@@ -44,9 +45,10 @@ public class EventAdminService {
4445
private final EventRepository eventRepository;
4546
private final ParticipationRepository participationRepository;
4647

47-
private final static int PAGE_SIZE = 10;
48+
private static final int PAGE_SIZE = 10;
4849

4950
private final ImageService imageService;
51+
private final RedisEventService redisEventService;
5052
private final UserClientService userClientService;
5153
private final EventStatusScheduler eventStatusScheduler;
5254

@@ -64,6 +66,7 @@ public EventResponse createEvent(EventCreateRequest request, MultipartFile event
6466

6567
Event savedEvent = eventRepository.save(event);
6668
eventStatusScheduler.scheduleCreateOrUpdatedEvent(savedEvent);
69+
redisEventService.initializeTickets(savedEvent.getId(), stock.getInitialStock());
6770

6871
return EventResponse.of(savedEvent);
6972
}
@@ -79,31 +82,26 @@ public EventPageResponse eventPageResponseWithStatus(String status, int pageNumb
7982
@Transactional
8083
public EventResponse updateEvent(Long eventId, EventUpdateRequest request, MultipartFile eventImage) {
8184
// 엔티티 조회, 권한 검증
82-
Event event = findEventById(eventId);
85+
Event findEvent = findEventById(eventId);
8386
String currentUserId = findAdminUser();
8487

8588
// 입력값 검증
8689
request.validateEventTimes();
87-
validationEvent(event, currentUserId);
88-
89-
// 수량 변경 대비
90-
int oldQuantity = event.getStock().getStock();
90+
validationEvent(findEvent, currentUserId);
9191

9292
// 이미지 처리
9393
if (eventImage != null && !eventImage.isEmpty()) {
9494
String newUrl = imageService.handleImageUpload(eventImage);
95-
event.updateImageUrl(newUrl);
95+
findEvent.updateImageUrl(newUrl);
9696
}
9797

9898
// 엔티티 업데이트
99-
event.updateFrom(request);
99+
findEvent.updateFrom(request);
100100

101-
// Redis 동기화 - (수량 변경 시)
102-
int newQuantity = event.getStock().getStock();
101+
eventStatusScheduler.scheduleCreateOrUpdatedEvent(findEvent);
102+
redisEventService.initializeTickets(findEvent.getId(), findEvent.getStock().getInitialStock());
103103

104-
eventStatusScheduler.scheduleCreateOrUpdatedEvent(event);
105-
106-
return EventResponse.of(event);
104+
return EventResponse.of(findEvent);
107105
}
108106

109107
@Transactional
@@ -112,6 +110,7 @@ public void deleteEvent(Long eventId) {
112110
event.delete();
113111

114112
eventStatusScheduler.scheduleAllDelete(event);
113+
redisEventService.deleteTickets(eventId);
115114
}
116115

117116
public String getEventPassword(Long eventId) {
@@ -126,8 +125,10 @@ public void closeEvent(Long eventId) {
126125
Event findEvent = findEventById(eventId);
127126
findEvent.delete();
128127
eventStatusScheduler.scheduleAllDelete(findEvent);
128+
redisEventService.deleteTickets(eventId);
129129
}
130130

131+
@Transactional(readOnly = true)
131132
public EventParticipationProfilePageResponse getParticipationList(Long eventId, int pageNumber) {
132133
Pageable pageable = PageRequest.of(pageNumber - 1, 10, Sort.by("ticketNumber").descending());
133134

src/main/java/inu/codin/codinticketingapi/domain/ticketing/controller/EventController.java

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
import io.swagger.v3.oas.annotations.Parameter;
1414
import io.swagger.v3.oas.annotations.responses.ApiResponse;
1515
import io.swagger.v3.oas.annotations.tags.Tag;
16-
import jakarta.validation.Valid;
1716
import jakarta.validation.constraints.NotNull;
1817
import lombok.RequiredArgsConstructor;
1918
import org.springframework.http.ResponseEntity;

0 commit comments

Comments
 (0)