mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2026-06-12 23:19:10 +00:00
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:
@@ -0,0 +1,64 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="ParticipationServiceApplication" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="env">
|
||||
<map>
|
||||
<!-- 서버 설정 -->
|
||||
<entry key="SERVER_PORT" value="8084" />
|
||||
|
||||
<!-- 데이터베이스 설정 -->
|
||||
<entry key="DB_HOST" value="4.230.72.147" />
|
||||
<entry key="DB_PORT" value="5432" />
|
||||
<entry key="DB_NAME" value="participationdb" />
|
||||
<entry key="DB_USERNAME" value="eventuser" />
|
||||
<entry key="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
|
||||
<!-- JPA 설정 -->
|
||||
<entry key="DDL_AUTO" value="update" />
|
||||
<entry key="SHOW_SQL" value="true" />
|
||||
|
||||
<!-- Kafka 설정 -->
|
||||
<entry key="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" />
|
||||
|
||||
<!-- JWT 설정 -->
|
||||
<entry key="JWT_SECRET" value="kt-event-marketing-secret-key-for-development-only-change-in-production" />
|
||||
<entry key="JWT_EXPIRATION" value="86400000" />
|
||||
|
||||
<!-- 로깅 설정 -->
|
||||
<entry key="LOG_LEVEL" value="INFO" />
|
||||
<entry key="LOG_FILE" value="logs/participation-service.log" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="participation-service:bootRun" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||
<extension name="net.ashald.envfile">
|
||||
<option name="IS_ENABLED" value="false" />
|
||||
<option name="IS_SUBST" value="false" />
|
||||
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
|
||||
<option name="IS_IGNORE_MISSING_FILES" value="false" />
|
||||
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
|
||||
<ENTRIES>
|
||||
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
|
||||
</ENTRIES>
|
||||
</extension>
|
||||
</EXTENSION>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -0,0 +1,56 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="participation-service" type="GradleRunConfiguration" factoryName="Gradle">
|
||||
<ExternalSystemSettings>
|
||||
<option name="env">
|
||||
<map>
|
||||
<entry key="DB_HOST" value="4.230.72.147" />
|
||||
<entry key="DB_NAME" value="participationdb" />
|
||||
<entry key="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
<entry key="DB_PORT" value="5432" />
|
||||
<entry key="DB_USERNAME" value="eventuser" />
|
||||
<entry key="DDL_AUTO" value="validate" />
|
||||
<entry key="JWT_EXPIRATION" value="86400000" />
|
||||
<entry key="JWT_SECRET" value="kt-event-marketing-secret-key-for-development-only-change-in-production" />
|
||||
<entry key="KAFKA_BOOTSTRAP_SERVERS" value="20.249.182.13:9095,4.217.131.59:9095" />
|
||||
<entry key="LOG_FILE" value="logs/participation-service.log" />
|
||||
<entry key="LOG_LEVEL" value="INFO" />
|
||||
<entry key="REDIS_HOST" value="20.214.210.71" />
|
||||
<entry key="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||
<entry key="REDIS_PORT" value="6379" />
|
||||
<entry key="SERVER_PORT" value="8084" />
|
||||
<entry key="SHOW_SQL" value="true" />
|
||||
</map>
|
||||
</option>
|
||||
<option name="executionName" />
|
||||
<option name="externalProjectPath" value="$PROJECT_DIR$/participation-service" />
|
||||
<option name="externalSystemIdString" value="GRADLE" />
|
||||
<option name="scriptParameters" value="" />
|
||||
<option name="taskDescriptions">
|
||||
<list />
|
||||
</option>
|
||||
<option name="taskNames">
|
||||
<list>
|
||||
<option value="participation-service:bootRun" />
|
||||
</list>
|
||||
</option>
|
||||
<option name="vmOptions" />
|
||||
</ExternalSystemSettings>
|
||||
<ExternalSystemDebugServerProcess>true</ExternalSystemDebugServerProcess>
|
||||
<ExternalSystemReattachDebugProcess>true</ExternalSystemReattachDebugProcess>
|
||||
<EXTENSION ID="com.intellij.execution.ExternalSystemRunConfigurationJavaExtension">
|
||||
<extension name="net.ashald.envfile">
|
||||
<option name="IS_ENABLED" value="false" />
|
||||
<option name="IS_SUBST" value="false" />
|
||||
<option name="IS_PATH_MACRO_SUPPORTED" value="false" />
|
||||
<option name="IS_IGNORE_MISSING_FILES" value="false" />
|
||||
<option name="IS_ENABLE_EXPERIMENTAL_INTEGRATIONS" value="false" />
|
||||
<ENTRIES>
|
||||
<ENTRY IS_ENABLED="true" PARSER="runconfig" IS_EXECUTABLE="false" />
|
||||
</ENTRIES>
|
||||
</extension>
|
||||
</EXTENSION>
|
||||
<DebugAllEnabled>false</DebugAllEnabled>
|
||||
<RunAsTest>false</RunAsTest>
|
||||
<method v="2" />
|
||||
</configuration>
|
||||
</component>
|
||||
@@ -43,6 +43,7 @@ dependencies {
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.kafka:spring-kafka-test'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
testRuntimeOnly 'com.h2database:h2'
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
|
||||
+19
-4
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
+11
-3
@@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
+17
-1
@@ -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 =
|
||||
|
||||
+15
-1
@@ -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:
|
||||
|
||||
+165
@@ -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);
|
||||
}
|
||||
}
|
||||
}
|
||||
+114
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user