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
@@ -0,0 +1,165 @@
package com.kt.event.participation.test.integration;
import com.kt.event.participation.application.dto.ParticipationRequest;
import com.kt.event.participation.application.service.ParticipationService;
import com.kt.event.participation.domain.participant.ParticipantRepository;
import com.kt.event.participation.infrastructure.kafka.event.ParticipantRegisteredEvent;
import org.apache.kafka.clients.consumer.Consumer;
import org.apache.kafka.clients.consumer.ConsumerConfig;
import org.apache.kafka.clients.consumer.ConsumerRecord;
import org.apache.kafka.common.serialization.StringDeserializer;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
import org.springframework.kafka.support.serializer.JsonDeserializer;
import org.springframework.kafka.test.utils.KafkaTestUtils;
import org.springframework.test.context.ActiveProfiles;
import java.time.Duration;
import java.util.Collections;
import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Kafka 이벤트 발행 통합 테스트
*
* @author Digital Garage Team
* @since 2025-01-24
*/
@SpringBootTest
@DisplayName("Kafka 이벤트 발행 통합 테스트")
class KafkaEventPublishIntegrationTest {
private static final String TOPIC = "participant-registered-events";
private static final String TEST_EVENT_ID = "EVT-TEST-001";
@Autowired
private ParticipationService participationService;
@Autowired
private ParticipantRepository participantRepository;
private Consumer<String, ParticipantRegisteredEvent> consumer;
@BeforeEach
void setUp() {
// Kafka Consumer 설정
Map<String, Object> consumerProps = KafkaTestUtils.consumerProps(
"20.249.182.13:9095", "test-group", "false");
consumerProps.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
consumerProps.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
consumerProps.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
consumerProps.put(JsonDeserializer.VALUE_DEFAULT_TYPE, ParticipantRegisteredEvent.class);
consumerProps.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "latest");
DefaultKafkaConsumerFactory<String, ParticipantRegisteredEvent> consumerFactory =
new DefaultKafkaConsumerFactory<>(consumerProps);
consumer = consumerFactory.createConsumer();
consumer.subscribe(Collections.singletonList(TOPIC));
}
@AfterEach
void tearDown() {
if (consumer != null) {
consumer.close();
}
// 테스트 데이터 정리
participantRepository.deleteAll();
}
@Test
@DisplayName("이벤트 참여 시 Kafka 이벤트가 발행되어야 한다")
void shouldPublishKafkaEventWhenParticipate() throws Exception {
// Given: 참여 요청 데이터
ParticipationRequest request = ParticipationRequest.builder()
.name("테스트사용자")
.phoneNumber("01012345678")
.email("test@example.com")
.storeVisited(true)
.agreeMarketing(true)
.agreePrivacy(true)
.build();
// When: 이벤트 참여
participationService.participate(TEST_EVENT_ID, request);
// Then: Kafka 메시지 수신 확인
ConsumerRecord<String, ParticipantRegisteredEvent> record =
KafkaTestUtils.getSingleRecord(consumer, TOPIC, Duration.ofSeconds(10));
assertThat(record).isNotNull();
assertThat(record.key()).isEqualTo(TEST_EVENT_ID);
ParticipantRegisteredEvent event = record.value();
assertThat(event).isNotNull();
assertThat(event.getEventId()).isEqualTo(TEST_EVENT_ID);
assertThat(event.getName()).isEqualTo("테스트사용자");
assertThat(event.getPhoneNumber()).isEqualTo("01012345678");
assertThat(event.getStoreVisited()).isTrue();
assertThat(event.getBonusEntries()).isEqualTo(5);
assertThat(event.getParticipatedAt()).isNotNull();
}
@Test
@DisplayName("매장 미방문 참여자의 이벤트가 발행되어야 한다")
void shouldPublishEventForNonStoreVisitor() throws Exception {
// Given: 매장 미방문 참여 요청
ParticipationRequest request = ParticipationRequest.builder()
.name("온라인사용자")
.phoneNumber("01098765432")
.email("online@example.com")
.storeVisited(false)
.agreeMarketing(false)
.agreePrivacy(true)
.build();
// When: 이벤트 참여
participationService.participate(TEST_EVENT_ID, request);
// Then: Kafka 메시지 수신 확인
ConsumerRecord<String, ParticipantRegisteredEvent> record =
KafkaTestUtils.getSingleRecord(consumer, TOPIC, Duration.ofSeconds(10));
assertThat(record).isNotNull();
ParticipantRegisteredEvent event = record.value();
assertThat(event.getStoreVisited()).isFalse();
assertThat(event.getBonusEntries()).isEqualTo(1);
}
@Test
@DisplayName("여러 참여자의 이벤트가 순차적으로 발행되어야 한다")
void shouldPublishMultipleEventsSequentially() throws Exception {
// Given: 3명의 참여자
for (int i = 1; i <= 3; i++) {
ParticipationRequest request = ParticipationRequest.builder()
.name("참여자" + i)
.phoneNumber("0101234567" + i)
.email("user" + i + "@example.com")
.storeVisited(i % 2 == 0)
.agreeMarketing(true)
.agreePrivacy(true)
.build();
// When: 이벤트 참여
participationService.participate(TEST_EVENT_ID, request);
}
// Then: 3개의 Kafka 메시지 수신 확인
for (int i = 1; i <= 3; i++) {
ConsumerRecord<String, ParticipantRegisteredEvent> record =
KafkaTestUtils.getSingleRecord(consumer, TOPIC, Duration.ofSeconds(10));
assertThat(record).isNotNull();
ParticipantRegisteredEvent event = record.value();
assertThat(event.getName()).startsWith("참여자");
assertThat(event.getEventId()).isEqualTo(TEST_EVENT_ID);
}
}
}
@@ -0,0 +1,114 @@
package com.kt.event.participation.test.integration;
import com.kt.event.participation.domain.participant.Participant;
import com.kt.event.participation.domain.participant.ParticipantRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.TestPropertySource;
/**
* Spring Data JPA 메서드의 실제 쿼리 확인용 테스트
*
* @author Digital Garage Team
* @since 2025-01-24
*/
@DataJpaTest
@TestPropertySource(properties = {
"spring.jpa.show-sql=true",
"spring.jpa.properties.hibernate.format_sql=true",
"logging.level.org.hibernate.SQL=DEBUG",
"logging.level.org.hibernate.type.descriptor.sql.BasicBinder=TRACE"
})
@DisplayName("JPA 쿼리 검증 테스트")
class QueryVerificationTest {
@Autowired
private ParticipantRepository participantRepository;
@Test
@DisplayName("countByEventIdAndIsWinnerTrue 메서드의 실제 쿼리 확인")
void verifyCountByEventIdAndIsWinnerTrueQuery() {
// Given
String eventId = "evt_test_001";
// 테스트 데이터 생성
for (int i = 1; i <= 5; i++) {
Participant participant = Participant.builder()
.participantId("prt_test_" + i)
.eventId(eventId)
.name("참여자" + i)
.phoneNumber("010-1234-" + String.format("%04d", i))
.email("test" + i + "@test.com")
.storeVisited(true)
.bonusEntries(2)
.agreeMarketing(true)
.agreePrivacy(true)
.isWinner(i <= 2)
.build();
participantRepository.save(participant);
}
// When - 이 쿼리가 실행되면서 콘솔에 SQL이 출력됨
System.out.println("\n========== countByEventIdAndIsWinnerTrue 실행 ==========");
long count = participantRepository.countByEventIdAndIsWinnerTrue(eventId);
System.out.println("========== 결과: " + count + " ==========\n");
}
@Test
@DisplayName("findByEventIdAndPhoneNumber 메서드의 실제 쿼리 확인")
void verifyExistsByEventIdAndPhoneNumberQuery() {
// Given
String eventId = "evt_test_002";
String phoneNumber = "010-1234-5678";
Participant participant = Participant.builder()
.participantId("prt_test_001")
.eventId(eventId)
.name("홍길동")
.phoneNumber(phoneNumber)
.email("hong@test.com")
.storeVisited(true)
.bonusEntries(2)
.agreeMarketing(true)
.agreePrivacy(true)
.isWinner(false)
.build();
participantRepository.save(participant);
// When
System.out.println("\n========== existsByEventIdAndPhoneNumber 실행 ==========");
boolean exists = participantRepository.existsByEventIdAndPhoneNumber(eventId, phoneNumber);
System.out.println("========== 결과: " + exists + " ==========\n");
}
@Test
@DisplayName("findByEventIdOrderByCreatedAtDesc 메서드의 실제 쿼리 확인")
void verifyFindByEventIdOrderByCreatedAtDescQuery() {
// Given
String eventId = "evt_test_003";
for (int i = 1; i <= 3; i++) {
Participant participant = Participant.builder()
.participantId("prt_test_" + i)
.eventId(eventId)
.name("참여자" + i)
.phoneNumber("010-1234-" + String.format("%04d", i))
.email("test" + i + "@test.com")
.storeVisited(true)
.bonusEntries(2)
.agreeMarketing(true)
.agreePrivacy(true)
.isWinner(false)
.build();
participantRepository.save(participant);
}
// When
System.out.println("\n========== findByEventIdOrderByCreatedAtDesc 실행 ==========");
participantRepository.findByEventIdOrderByCreatedAtDesc(eventId,
org.springframework.data.domain.PageRequest.of(0, 10));
System.out.println("========== 쿼리 실행 완료 ==========\n");
}
}
@@ -16,10 +16,14 @@ spring:
username: sa
password:
# Kafka 자동설정 비활성화 (통합 테스트에서는 불필요)
autoconfigure:
exclude:
- org.springframework.boot.autoconfigure.kafka.KafkaAutoConfiguration
# Kafka 설정 (통합 테스트)
kafka:
bootstrap-servers: 20.249.182.13:9095
producer:
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
acks: all
retries: 3
# H2 콘솔 활성화 (디버깅용)
h2:
@@ -27,9 +31,16 @@ spring:
enabled: true
path: /h2-console
# JWT 설정 (테스트용)
jwt:
secret: test-secret-key-for-testing-only-minimum-256-bits
expiration: 86400000
# 로깅 레벨
logging:
level:
org.hibernate.SQL: DEBUG
org.hibernate.type.descriptor.sql.BasicBinder: TRACE
com.kt.event.participation: DEBUG
org.springframework.kafka: DEBUG
org.apache.kafka: DEBUG