Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@
import inu.codin.codinticketingapi.domain.ticketing.dto.stream.EventStockStream;
import inu.codin.codinticketingapi.domain.ticketing.entity.Campus;
import inu.codin.codinticketingapi.domain.ticketing.entity.ParticipationStatus;
import inu.codin.codinticketingapi.domain.ticketing.service.EventService;
import inu.codin.codinticketingapi.domain.ticketing.service.EventQueryService;
import inu.codin.codinticketingapi.domain.ticketing.service.EventStockProducerService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand All @@ -25,7 +25,7 @@
@Tag(name = "Event API", description = "티켓팅 이벤트 API")
public class EventController {

private final EventService eventService;
private final EventQueryService eventQueryService;

private final EventStockProducerService eventStockProducerService;

Expand All @@ -38,7 +38,7 @@ public ResponseEntity<SingleResponse<EventPageResponse>> getEventList(
@Parameter(description = "페이지", example = "0") @RequestParam("page") @NotNull int pageNumber
) {
return ResponseEntity.ok(new SingleResponse<>(200, "티켓팅 이벤트 게시물 리스트 조회 성공",
eventService.getEventList(campus, pageNumber)));
eventQueryService.getEventList(campus, pageNumber)));
}

/** 티켓팅 이벤트 상세 정보 조회 */
Expand All @@ -49,7 +49,7 @@ public ResponseEntity<SingleResponse<EventDetailResponse>> getEventDetail(
@Parameter(description = "이벤트 ID", example = "1111") @PathVariable Long eventId
) {
return ResponseEntity.ok(new SingleResponse<>(200, "티켓팅 이벤트 상세 정보 조회 성공",
eventService.getEventDetail(eventId)));
eventQueryService.getEventDetail(eventId)));
}

/** 유저 마이페이지 티켓팅 참여 전체 이력 조회 */
Expand All @@ -60,7 +60,7 @@ public ResponseEntity<SingleResponse<EventParticipationHistoryPageResponse>> get
@Parameter(description = "페이지", example = "0") @RequestParam("page") @NotNull int pageNumber
) {
return ResponseEntity.ok(new SingleResponse<>(200, "티켓팅 유저 티켓팅 참여 전체 이력 조회",
eventService.getUserEventList(pageNumber)));
eventQueryService.getUserEventList(pageNumber)));
}

/** 유저 마이페이지 티켓팅 참여 완료, 미수령, 취소 이력 조회 */
Expand All @@ -72,7 +72,7 @@ public ResponseEntity<SingleResponse<EventParticipationHistoryPageResponse>> get
@Parameter(description = "티켓팀 참여 상태", example = "COMPLETED") @RequestParam("status") ParticipationStatus status
) {
return ResponseEntity.ok(new SingleResponse<>(200, "유저 티켓팅 참여 (완료, 취소) 이력 조회",
eventService.getUserEventListByStatus(pageNumber, status)));
eventQueryService.getUserEventListByStatus(pageNumber, status)));
}

/** [테스트] 재고상태 구독자들에게 SSE 전송 */
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,14 @@
import inu.codin.codinticketingapi.domain.ticketing.entity.ParticipationStatus;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

import java.time.LocalDateTime;

@Getter
@AllArgsConstructor
@Builder
@Schema(description = "티켓팅 이벤트 참여 기록 응답 DTO")
public class EventParticipationHistoryDto {

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,8 @@ public enum TicketingErrorCode implements GlobalErrorCode {
PASSWORD_INVALID(HttpStatus.BAD_REQUEST, "관리자 비밀번호가 맞지 않습니다."),
UNAUTHORIZED_EVENT_UPDATE(HttpStatus.UNAUTHORIZED, "인증되지 않은 이벤트 업데이트 입니다."),
EVENT_ALREADY_STARTED(HttpStatus.BAD_REQUEST, "이미 이벤트가 시작했습니다."),
INSUFFICIENT_TOTAL_STOCK(HttpStatus.BAD_REQUEST, "변경하려는 재고는 남은 재고보다 적을 수 없습니다.");
INSUFFICIENT_TOTAL_STOCK(HttpStatus.BAD_REQUEST, "변경하려는 재고는 남은 재고보다 적을 수 없습니다."),
PAGE_ILLEGAL_ARGUMENT(HttpStatus.BAD_REQUEST, "페이지 번호는 1 이상이어야 합니다.");

private final HttpStatus httpStatus;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
package inu.codin.codinticketingapi.domain.ticketing.redis;

import inu.codin.codinticketingapi.domain.ticketing.service.EventService;
import inu.codin.codinticketingapi.domain.ticketing.service.EventCommandService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.RedisConnectionFailureException;
Expand All @@ -10,11 +10,12 @@

import java.util.concurrent.atomic.AtomicInteger;

@Slf4j
@Service
@Slf4j
@RequiredArgsConstructor
public class RedisHealthCheckService {
private final EventService eventService;

private final EventCommandService eventCommandService;
private final RedisTemplate<String, String> pingRedisTemplate;

// 연속 실패 횟수를 저장 (동시성 문제를 위해 AtomicInteger 사용)
Expand All @@ -39,10 +40,9 @@ public void checkRedisHealth() {

private void handleSuccess() {
int currentFailures = consecutiveFailures.get();

if (currentFailures >= FAILURE_THRESHOLD) {
log.info("Redis 연결이 복구되었습니다. 이벤트 상태를 ACTIVE로 복원합니다.");
eventService.restoreUpcomingEventsToActive();
eventCommandService.restoreUpcomingEventsToActive();
} else if (currentFailures > 0) {
log.info("Redis 연결이 복구되었습니다.");
}
Expand All @@ -53,11 +53,10 @@ private void handleSuccess() {
private void handleFailure() {
int failures = consecutiveFailures.incrementAndGet();
log.warn("Redis 연결 실패. 연속 실패 횟수: {}회", failures);

// 정확히 임계값에 도달했을 때 딱 한 번만 실행
if (failures == FAILURE_THRESHOLD) {
log.error("Redis 장애가 {}초 이상 지속되어 모든 활성 이벤트를 UPCOMING 상태로 변경합니다.", 10 * FAILURE_THRESHOLD);
eventService.changeAllActiveEventsToUpcoming();
eventCommandService.changeAllActiveEventsToUpcoming();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -40,9 +40,10 @@ public void execute(JobExecutionContext context) throws JobExecutionException {

lastStockMap.compute(eventId, (id, prev) -> {
// 최초 등록 시에는 prev == null
if (prev == null) {
return current;
}
lastStockMap.putIfAbsent(id, current);
// if (prev == null) {
// return current;
// }
// 재고 변화가 감지되면 이벤트 발행 및 값 갱신
if (prev != current) {
producerService.publishEventStock(new EventStockStream(eventId, (long) current));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package inu.codin.codinticketingapi.domain.ticketing.service;

import inu.codin.codinticketingapi.domain.admin.entity.Event;
import inu.codin.codinticketingapi.domain.admin.entity.EventStatus;
import inu.codin.codinticketingapi.domain.ticketing.repository.EventRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

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

@Service
@Slf4j
@RequiredArgsConstructor
public class EventCommandService {

private final EventRepository eventRepository;

@Transactional
public void changeAllActiveEventsToUpcoming() {
List<Event> activeEvents = eventRepository.findByEventStatus(EventStatus.ACTIVE);
if (activeEvents.isEmpty()) {
log.info("상태를 변경할 활성 이벤트가 없습니다.");
return;
}
// 각 이벤트의 상태를 UPCOMING으로 변경
activeEvents.forEach(event -> {
log.info("이벤트 ID: {}, '{}'의 상태를 ACTIVE에서 UPCOMING으로 변경합니다.", event.getId(), event.getTitle());
event.updateStatus(EventStatus.UPCOMING);
});
}

@Transactional
public void restoreUpcomingEventsToActive() {
List<Event> upcomingEvents = eventRepository.findAllLiveEvent(EventStatus.UPCOMING, LocalDateTime.now());
if (upcomingEvents.isEmpty()) {
log.info("ACTIVE로 복구할 UPCOMING 상태의 이벤트가 없습니다.");
return;
}
upcomingEvents.forEach(event -> {
log.info("Redis 복구로 인해 이벤트 ID: {}의 상태를 ACTIVE로 복구합니다.", event.getId());
event.updateStatus(EventStatus.ACTIVE);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,10 @@
import java.time.LocalDateTime;
import java.util.List;

@Slf4j
@Service
@Slf4j
@RequiredArgsConstructor
public class EventService {
public class EventQueryService {

private final EventRepository eventRepository;
private final ParticipationRepository participationRepository;
Expand Down Expand Up @@ -63,48 +63,21 @@ public EventDetailResponse getEventDetail(Long eventId) {

@Transactional(readOnly = true)
public EventParticipationHistoryPageResponse getUserEventList(int pageNumber) {
if (pageNumber < 1) {
throw new TicketingException(TicketingErrorCode.PAGE_ILLEGAL_ARGUMENT);
}
String userId = userClientService.fetchUser().getUserId();
Pageable pageable = PageRequest.of(pageNumber, 10, Sort.by("createdAt").descending());
return EventParticipationHistoryPageResponse.of(participationRepository.findHistoryByUserId(userId, pageable));
}

@Transactional(readOnly = true)
public EventParticipationHistoryPageResponse getUserEventListByStatus(int pageNumber, @NotNull ParticipationStatus status) {
if (pageNumber < 1) {
throw new TicketingException(TicketingErrorCode.PAGE_ILLEGAL_ARGUMENT);
}
String userId = userClientService.fetchUser().getUserId();
Pageable pageable = PageRequest.of(pageNumber, 10, Sort.by("createdAt").descending());
return EventParticipationHistoryPageResponse.of(participationRepository.findHistoryByUserIdAndCanceled(userId, status, pageable));
}

@Transactional
public void changeAllActiveEventsToUpcoming() {
List<Event> activeEvents = eventRepository.findByEventStatus(EventStatus.ACTIVE);

if (activeEvents.isEmpty()) {
log.info("상태를 변경할 활성 이벤트가 없습니다.");

return;
}

// 각 이벤트의 상태를 UPCOMING으로 변경
activeEvents.forEach(event -> {
log.info("이벤트 ID: {}, '{}'의 상태를 ACTIVE에서 UPCOMING으로 변경합니다.", event.getId(), event.getTitle());
event.updateStatus(EventStatus.UPCOMING);
});
}

@Transactional
public void restoreUpcomingEventsToActive() {
List<Event> upcomingEvents = eventRepository.findAllLiveEvent(EventStatus.UPCOMING, LocalDateTime.now());

if (upcomingEvents.isEmpty()) {
log.info("ACTIVE로 복구할 UPCOMING 상태의 이벤트가 없습니다.");

return;
}

upcomingEvents.forEach(event -> {
log.info("Redis 복구로 인해 이벤트 ID: {}의 상태를 ACTIVE로 복구합니다.", event.getId());
event.updateStatus(EventStatus.ACTIVE);
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,15 +24,11 @@ public class EventStockProducerService {
* EventStockStream DTO를 Redis Stream에 전송
*/
public void publishEventStock(EventStockStream eventStockStream) {
// stock_event_log 테이블 추가해 로그 관리
ObjectRecord<String, EventStockStream> record = StreamRecords
.newRecord()
.in(STREAM_KEY)
.ofObject(eventStockStream);

RecordId recordId = redisTemplate.opsForStream().add(record);

// RecordId recordId = redisTemplate.opsForStream().add(record);
log.info("[Producer] Published EventStockStream: {}", eventStockStream);
// return recordId;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ public ParticipationResponse saveParticipation(Long eventId) {

// 이벤트 상태 검증
if (findEvent.getEventStatus() != EventStatus.ACTIVE) {

throw new TicketingException(TicketingErrorCode.EVENT_NOT_ACTIVE);
}

Expand Down
Loading