WinnerController Swagger 문서화 추가 및 이벤트/참여자 예외 처리 개선

- WinnerController에 Swagger 어노테이션 추가 (Operation, Parameter, ParameterObject)
- 당첨자 목록 조회 API 기본 정렬 설정 (winnerRank ASC, size=20)
- ParticipationService에서 이벤트/참여자 구분 로직 개선
  - 이벤트 없음: EventNotFoundException 발생
  - 참여자 없음: ParticipantNotFoundException 발생
- EventCacheService 제거 (Redis 기반 검증에서 DB 기반 검증으로 변경)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
doyeon
2025-10-27 11:15:04 +09:00
parent 958184c9d1
commit 9039424c40
18 changed files with 1330 additions and 45 deletions
@@ -6,6 +6,8 @@ import com.kt.event.participation.application.dto.ParticipationResponse;
import com.kt.event.participation.domain.participant.Participant;
import com.kt.event.participation.domain.participant.ParticipantRepository;
import com.kt.event.participation.exception.ParticipationException.*;
import static com.kt.event.participation.exception.ParticipationException.EventNotFoundException;
import static com.kt.event.participation.exception.ParticipationException.ParticipantNotFoundException;
import com.kt.event.participation.infrastructure.kafka.KafkaProducerService;
import com.kt.event.participation.infrastructure.kafka.event.ParticipantRegisteredEvent;
import lombok.RequiredArgsConstructor;
@@ -15,6 +17,8 @@ import org.springframework.data.domain.Pageable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.Optional;
/**
* 이벤트 참여 서비스
*
@@ -108,10 +112,21 @@ public class ParticipationService {
*/
@Transactional(readOnly = true)
public ParticipationResponse getParticipant(String eventId, String participantId) {
Participant participant = participantRepository
.findByEventIdAndParticipantId(eventId, participantId)
.orElseThrow(ParticipantNotFoundException::new);
// 참여자 조회
Optional<Participant> participantOpt = participantRepository
.findByEventIdAndParticipantId(eventId, participantId);
return ParticipationResponse.from(participant);
// 참여자가 없으면 이벤트 존재 여부 확인
if (participantOpt.isEmpty()) {
long participantCount = participantRepository.countByEventId(eventId);
if (participantCount == 0) {
// 이벤트에 참여자가 한 명도 없음 = 이벤트가 존재하지 않음
throw new EventNotFoundException();
}
// 이벤트는 존재하지만 해당 참여자가 없음
throw new ParticipantNotFoundException();
}
return ParticipationResponse.from(participantOpt.get());
}
}
@@ -115,9 +115,17 @@ public class Participant extends BaseTimeEntity {
* @return 생성된 참여자 ID
*/
public static String generateParticipantId(String eventId, Long sequenceNumber) {
// evt_20250123_001 → prt_20250123_001
String dateTime = eventId.substring(4, 12); // 20250123
return String.format("prt_%s_%03d", dateTime, sequenceNumber);
// eventId가 "evt_YYYYMMDD_XXX" 형식인 경우
if (eventId != null && eventId.length() >= 16 && eventId.startsWith("evt_")) {
String dateTime = eventId.substring(4, 12); // "20250124"
String eventSeq = eventId.substring(13); // "002"
return String.format("prt_%s_%s_%03d", dateTime, eventSeq, sequenceNumber);
}
// 그 외의 경우 (짧은 eventId 등): 현재 날짜 사용
String dateTime = java.time.LocalDate.now().format(
java.time.format.DateTimeFormatter.ofPattern("yyyyMMdd"));
return String.format("prt_%s_%s_%03d", eventId, dateTime, sequenceNumber);
}
/**
@@ -5,10 +5,15 @@ import com.kt.event.common.dto.PageResponse;
import com.kt.event.participation.application.dto.ParticipationRequest;
import com.kt.event.participation.application.dto.ParticipationResponse;
import com.kt.event.participation.application.service.ParticipationService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springdoc.core.annotations.ParameterObject;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
@@ -49,11 +54,22 @@ public class ParticipationController {
* 참여자 목록 조회
* GET /events/{eventId}/participants
*/
@Operation(
summary = "참여자 목록 조회",
description = "이벤트의 참여자 목록을 페이징하여 조회합니다. " +
"정렬 가능한 필드: createdAt(기본값), participantId, name, phoneNumber, bonusEntries, isWinner, wonAt"
)
@GetMapping("/events/{eventId}/participants")
public ResponseEntity<ApiResponse<PageResponse<ParticipationResponse>>> getParticipants(
@Parameter(description = "이벤트 ID", example = "evt_20250124_001")
@PathVariable String eventId,
@Parameter(description = "매장 방문 여부 필터 (true: 방문자만, false: 미방문자만, null: 전체)")
@RequestParam(required = false) Boolean storeVisited,
@PageableDefault(size = 20) Pageable pageable) {
@ParameterObject
@PageableDefault(size = 20, sort = "createdAt", direction = Sort.Direction.DESC)
Pageable pageable) {
log.info("참여자 목록 조회 요청 - eventId: {}, storeVisited: {}", eventId, storeVisited);
PageResponse<ParticipationResponse> response =
@@ -6,10 +6,15 @@ import com.kt.event.participation.application.dto.DrawWinnersRequest;
import com.kt.event.participation.application.dto.DrawWinnersResponse;
import com.kt.event.participation.application.dto.ParticipationResponse;
import com.kt.event.participation.application.service.WinnerDrawService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springdoc.core.annotations.ParameterObject;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.PageableDefault;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@@ -47,10 +52,19 @@ public class WinnerController {
* 당첨자 목록 조회
* GET /events/{eventId}/winners
*/
@Operation(
summary = "당첨자 목록 조회",
description = "이벤트의 당첨자 목록을 페이징하여 조회합니다. " +
"정렬 가능한 필드: winnerRank(기본값), wonAt, participantId, name, phoneNumber, bonusEntries"
)
@GetMapping("/events/{eventId}/winners")
public ResponseEntity<ApiResponse<PageResponse<ParticipationResponse>>> getWinners(
@Parameter(description = "이벤트 ID", example = "evt_20250124_001")
@PathVariable String eventId,
@PageableDefault(size = 20) Pageable pageable) {
@ParameterObject
@PageableDefault(size = 20, sort = "winnerRank", direction = Sort.Direction.ASC)
Pageable pageable) {
log.info("당첨자 목록 조회 요청 - eventId: {}", eventId);
PageResponse<ParticipationResponse> response = winnerDrawService.getWinners(eventId, pageable);
@@ -18,7 +18,7 @@ spring:
# JPA 설정
jpa:
hibernate:
ddl-auto: ${DDL_AUTO:update}
ddl-auto: ${DDL_AUTO:validate}
show-sql: ${SHOW_SQL:true}
properties:
hibernate:
@@ -26,9 +26,23 @@ spring:
dialect: org.hibernate.dialect.PostgreSQLDialect
default_batch_fetch_size: 100
# Redis 설정
data:
redis:
host: ${REDIS_HOST:20.214.210.71}
port: ${REDIS_PORT:6379}
password: ${REDIS_PASSWORD:Hi5Jessica!}
timeout: 3000ms
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 2
max-wait: -1ms
# Kafka 설정
kafka:
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:4.230.50.63:9092}
bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:4.217.131.59:9095}
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
@@ -50,6 +64,8 @@ logging:
com.kt.event.participation: ${LOG_LEVEL:INFO}
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
org.springframework.kafka: DEBUG
org.apache.kafka: DEBUG
file:
name: ${LOG_FILE:logs/participation-service.log}
logback: