diff --git a/.claude/settings.local.json b/.claude/settings.local.json index 631f8a1..8d1f14d 100644 --- a/.claude/settings.local.json +++ b/.claude/settings.local.json @@ -15,9 +15,7 @@ "Bash(git add:*)", "Bash(git commit:*)", "Bash(git push)", - "Bash(git pull:*)", - "Bash(dir:*)", - "Bash(./gradlew participation-service:compileJava:*)" + "Bash(git pull:*)" ], "deny": [], "ask": [] diff --git a/.gradle/8.10/checksums/checksums.lock b/.gradle/8.10/checksums/checksums.lock index 4a61e20..837e5b9 100644 Binary files a/.gradle/8.10/checksums/checksums.lock and b/.gradle/8.10/checksums/checksums.lock differ diff --git a/.gradle/8.10/checksums/md5-checksums.bin b/.gradle/8.10/checksums/md5-checksums.bin index d7041a7..04c6d00 100644 Binary files a/.gradle/8.10/checksums/md5-checksums.bin and b/.gradle/8.10/checksums/md5-checksums.bin differ diff --git a/.gradle/8.10/checksums/sha1-checksums.bin b/.gradle/8.10/checksums/sha1-checksums.bin index ec94342..19a5410 100644 Binary files a/.gradle/8.10/checksums/sha1-checksums.bin and b/.gradle/8.10/checksums/sha1-checksums.bin differ diff --git a/.gradle/8.10/executionHistory/executionHistory.bin b/.gradle/8.10/executionHistory/executionHistory.bin index d409ba4..2177cdd 100644 Binary files a/.gradle/8.10/executionHistory/executionHistory.bin and b/.gradle/8.10/executionHistory/executionHistory.bin differ diff --git a/.gradle/8.10/executionHistory/executionHistory.lock b/.gradle/8.10/executionHistory/executionHistory.lock index ce0d272..0ce4c96 100644 Binary files a/.gradle/8.10/executionHistory/executionHistory.lock and b/.gradle/8.10/executionHistory/executionHistory.lock differ diff --git a/.gradle/8.10/fileHashes/fileHashes.bin b/.gradle/8.10/fileHashes/fileHashes.bin index e20a4f0..8088fbb 100644 Binary files a/.gradle/8.10/fileHashes/fileHashes.bin and b/.gradle/8.10/fileHashes/fileHashes.bin differ diff --git a/.gradle/8.10/fileHashes/fileHashes.lock b/.gradle/8.10/fileHashes/fileHashes.lock index af5d5a0..340e0dd 100644 Binary files a/.gradle/8.10/fileHashes/fileHashes.lock and b/.gradle/8.10/fileHashes/fileHashes.lock differ diff --git a/.gradle/8.10/fileHashes/resourceHashesCache.bin b/.gradle/8.10/fileHashes/resourceHashesCache.bin index aaa316e..3d21896 100644 Binary files a/.gradle/8.10/fileHashes/resourceHashesCache.bin and b/.gradle/8.10/fileHashes/resourceHashesCache.bin differ diff --git a/.gradle/buildOutputCleanup/buildOutputCleanup.lock b/.gradle/buildOutputCleanup/buildOutputCleanup.lock index c1f212d..0350ff2 100644 Binary files a/.gradle/buildOutputCleanup/buildOutputCleanup.lock and b/.gradle/buildOutputCleanup/buildOutputCleanup.lock differ diff --git a/.gradle/buildOutputCleanup/outputFiles.bin b/.gradle/buildOutputCleanup/outputFiles.bin index 6bb372f..4ed6f06 100644 Binary files a/.gradle/buildOutputCleanup/outputFiles.bin and b/.gradle/buildOutputCleanup/outputFiles.bin differ diff --git a/.gradle/file-system.probe b/.gradle/file-system.probe index f13d4e3..ac4beb4 100644 Binary files a/.gradle/file-system.probe and b/.gradle/file-system.probe differ diff --git a/.run/ParticipationServiceApplication.run.xml b/.run/ParticipationServiceApplication.run.xml deleted file mode 100644 index 624b419..0000000 --- a/.run/ParticipationServiceApplication.run.xml +++ /dev/null @@ -1,71 +0,0 @@ - - - - diff --git a/participation-service/src/main/java/com/kt/event/participation/ParticipationServiceApplication.java b/participation-service/src/main/java/com/kt/event/participation/ParticipationServiceApplication.java deleted file mode 100644 index e82842f..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/ParticipationServiceApplication.java +++ /dev/null @@ -1,25 +0,0 @@ -package com.kt.event.participation; - -import org.springframework.boot.SpringApplication; -import org.springframework.boot.autoconfigure.SpringBootApplication; -import org.springframework.data.jpa.repository.config.EnableJpaAuditing; - -/** - * Participation Service Application - * - 이벤트 참여 및 당첨자 관리 서비스 - * - Port: 8084 - * - Database: PostgreSQL (participation_db) - * - Cache: Redis - * - Messaging: Kafka (participant-events topic) - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@SpringBootApplication -@EnableJpaAuditing -public class ParticipationServiceApplication { - - public static void main(String[] args) { - SpringApplication.run(ParticipationServiceApplication.class, args); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/dto/ErrorResponse.java b/participation-service/src/main/java/com/kt/event/participation/application/dto/ErrorResponse.java deleted file mode 100644 index e1e906f..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/dto/ErrorResponse.java +++ /dev/null @@ -1,38 +0,0 @@ -package com.kt.event.participation.application.dto; - -import java.time.LocalDateTime; - -/** - * API 오류 응답 DTO - */ -public class ErrorResponse { - - private boolean success; - private String errorCode; - private String message; - private LocalDateTime timestamp; - - public ErrorResponse(String errorCode, String message) { - this.success = false; - this.errorCode = errorCode; - this.message = message; - this.timestamp = LocalDateTime.now(); - } - - // Getters - public boolean isSuccess() { - return success; - } - - public String getErrorCode() { - return errorCode; - } - - public String getMessage() { - return message; - } - - public LocalDateTime getTimestamp() { - return timestamp; - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipantDto.java b/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipantDto.java deleted file mode 100644 index b37245a..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipantDto.java +++ /dev/null @@ -1,76 +0,0 @@ -package com.kt.event.participation.application.dto; - -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * 참여자 정보 DTO - * 참여자 목록 조회 시 사용되는 기본 참여자 정보 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ParticipantDto { - - /** - * 참여자 ID - */ - private String participantId; - - /** - * 참여자 이름 - */ - private String name; - - /** - * 마스킹된 전화번호 - * 예: 010-****-5678 - */ - private String maskedPhoneNumber; - - /** - * 참여자 이메일 - */ - private String email; - - /** - * 유입 경로 - * 예: ONLINE, STORE_VISIT - */ - private String entryPath; - - /** - * 참여 일시 - * ISO 8601 형식 (yyyy-MM-dd'T'HH:mm:ss) - */ - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - private LocalDateTime participatedAt; - - /** - * 당첨 여부 - */ - private Boolean isWinner; - - /** - * 당첨 일시 - * 당첨되지 않은 경우 null - */ - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - private LocalDateTime wonAt; - - /** - * 매장 방문 여부 - */ - private Boolean storeVisited; - - /** - * 보너스 응모권 수 - */ - private Integer bonusEntries; -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipantListResponse.java b/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipantListResponse.java deleted file mode 100644 index 1833db9..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipantListResponse.java +++ /dev/null @@ -1,54 +0,0 @@ -package com.kt.event.participation.application.dto; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.util.List; - -/** - * 참여자 목록 응답 DTO - * 페이징된 참여자 목록과 페이지 정보 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ParticipantListResponse { - - /** - * 참여자 목록 - */ - private List participants; - - /** - * 전체 참여자 수 - */ - private Integer totalElements; - - /** - * 전체 페이지 수 - */ - private Integer totalPages; - - /** - * 현재 페이지 번호 (0부터 시작) - */ - private Integer currentPage; - - /** - * 페이지 크기 - */ - private Integer pageSize; - - /** - * 다음 페이지 존재 여부 - */ - private Boolean hasNext; - - /** - * 이전 페이지 존재 여부 - */ - private Boolean hasPrevious; -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipationRequest.java b/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipationRequest.java deleted file mode 100644 index 0a106cb..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipationRequest.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.kt.event.participation.application.dto; - -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; -import jakarta.validation.constraints.Size; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 이벤트 참여 요청 DTO - * 고객이 이벤트에 참여할 때 전달하는 정보 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ParticipationRequest { - - /** - * 참여자 이름 - * 필수 입력, 2-50자 제한 - */ - @NotBlank(message = "이름은 필수 입력입니다") - @Size(min = 2, max = 50, message = "이름은 2-50자 사이여야 합니다") - private String name; - - /** - * 참여자 전화번호 - * 필수 입력, 하이픈 포함 형식 (예: 010-1234-5678) - */ - @NotBlank(message = "전화번호는 필수 입력입니다") - @Pattern(regexp = "^\\d{3}-\\d{3,4}-\\d{4}$", message = "전화번호 형식이 올바르지 않습니다 (예: 010-1234-5678)") - private String phoneNumber; - - /** - * 참여자 이메일 - * 선택 입력, 이메일 형식 검증 - */ - @Email(message = "이메일 형식이 올바르지 않습니다") - private String email; - - /** - * 마케팅 정보 수신 동의 - * 선택, 기본값 false - */ - @Builder.Default - private Boolean agreeMarketing = false; - - /** - * 개인정보 수집 및 이용 동의 - * 필수 동의 - */ - @NotNull(message = "개인정보 수집 및 이용 동의는 필수입니다") - private Boolean agreePrivacy; - - /** - * 매장 방문 여부 - * 매장 방문 시 보너스 응모권 추가 제공 - * 기본값 false - */ - @Builder.Default - private Boolean storeVisited = false; -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipationResponse.java b/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipationResponse.java deleted file mode 100644 index 69b5125..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/dto/ParticipationResponse.java +++ /dev/null @@ -1,64 +0,0 @@ -package com.kt.event.participation.application.dto; - -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * 이벤트 참여 응답 DTO - * 참여 완료 후 반환되는 참여자 정보 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ParticipationResponse { - - /** - * 참여자 ID - * 고유 식별자 (예: prt_20250123_001) - */ - private String participantId; - - /** - * 이벤트 ID - */ - private String eventId; - - /** - * 참여자 이름 - */ - private String name; - - /** - * 참여자 전화번호 - */ - private String phoneNumber; - - /** - * 참여자 이메일 - */ - private String email; - - /** - * 참여 일시 - * ISO 8601 형식 (yyyy-MM-dd'T'HH:mm:ss) - */ - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - private LocalDateTime participatedAt; - - /** - * 매장 방문 여부 - */ - private Boolean storeVisited; - - /** - * 보너스 응모권 수 - * 기본 1회, 매장 방문 시 +1 추가 - */ - private Integer bonusEntries; -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/dto/WinnerDrawRequest.java b/participation-service/src/main/java/com/kt/event/participation/application/dto/WinnerDrawRequest.java deleted file mode 100644 index b9218db..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/dto/WinnerDrawRequest.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.kt.event.participation.application.dto; - -import jakarta.validation.constraints.Min; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -/** - * 당첨자 추첨 요청 DTO - * 당첨자 추첨 시 필요한 정보 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class WinnerDrawRequest { - - /** - * 당첨자 수 - * 필수 입력, 최소 1명 이상 - */ - @NotNull(message = "당첨자 수는 필수 입력입니다") - @Min(value = 1, message = "당첨자 수는 최소 1명 이상이어야 합니다") - private Integer winnerCount; - - /** - * 매장 방문 보너스 적용 여부 - * 매장 방문자에게 가중치 부여 - * 기본값 true - */ - @Builder.Default - private Boolean visitBonusApplied = true; -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/dto/WinnerDrawResponse.java b/participation-service/src/main/java/com/kt/event/participation/application/dto/WinnerDrawResponse.java deleted file mode 100644 index 50de1fe..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/dto/WinnerDrawResponse.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.kt.event.participation.application.dto; - -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; -import java.util.List; - -/** - * 당첨자 추첨 응답 DTO - * 추첨 완료 후 반환되는 당첨자 정보 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class WinnerDrawResponse { - - /** - * 당첨자 목록 - */ - private List winners; - - /** - * 추첨 로그 ID - * 추첨 이력 추적용 고유 식별자 - */ - private String drawLogId; - - /** - * 응답 메시지 - */ - private String message; - - /** - * 이벤트 ID - */ - private String eventId; - - /** - * 전체 참여자 수 - */ - private Integer totalParticipants; - - /** - * 당첨자 수 - */ - private Integer winnerCount; - - /** - * 추첨 일시 - * ISO 8601 형식 (yyyy-MM-dd'T'HH:mm:ss) - */ - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - private LocalDateTime drawnAt; -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/dto/WinnerDto.java b/participation-service/src/main/java/com/kt/event/participation/application/dto/WinnerDto.java deleted file mode 100644 index cf05044..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/dto/WinnerDto.java +++ /dev/null @@ -1,70 +0,0 @@ -package com.kt.event.participation.application.dto; - -import com.fasterxml.jackson.annotation.JsonFormat; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * 당첨자 정보 DTO - * 당첨자 목록 및 추첨 결과에 사용되는 당첨자 기본 정보 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class WinnerDto { - - /** - * 참여자 ID - */ - private String participantId; - - /** - * 당첨자 이름 - */ - private String name; - - /** - * 마스킹된 전화번호 - * 예: 010-****-5678 - */ - private String maskedPhoneNumber; - - /** - * 당첨자 이메일 - */ - private String email; - - /** - * 응모 번호 - * 추첨 시 사용된 번호 - */ - private Integer applicationNumber; - - /** - * 당첨 순위 - * 1부터 시작 - */ - private Integer rank; - - /** - * 당첨 일시 - * ISO 8601 형식 (yyyy-MM-dd'T'HH:mm:ss) - */ - @JsonFormat(pattern = "yyyy-MM-dd'T'HH:mm:ss") - private LocalDateTime wonAt; - - /** - * 매장 방문 여부 - */ - private Boolean storeVisited; - - /** - * 보너스 응모권 적용 여부 - */ - private Boolean bonusApplied; -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/service/LotteryAlgorithm.java b/participation-service/src/main/java/com/kt/event/participation/application/service/LotteryAlgorithm.java deleted file mode 100644 index 853ec74..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/service/LotteryAlgorithm.java +++ /dev/null @@ -1,117 +0,0 @@ -package com.kt.event.participation.application.service; - -import com.kt.event.participation.domain.participant.Participant; -import lombok.extern.slf4j.Slf4j; -import org.springframework.stereotype.Component; - -import java.security.SecureRandom; -import java.util.ArrayList; -import java.util.List; - -/** - * 당첨자 추첨 알고리즘 - * - Fisher-Yates Shuffle 알고리즘 사용 - * - 암호학적 난수 생성 (Crypto.randomBytes 대신 SecureRandom 사용) - * - 매장 방문 가산점 적용 - * - * 시간 복잡도: O(n log n) - * 공간 복잡도: O(n) - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@Slf4j -@Component -public class LotteryAlgorithm { - - private static final SecureRandom secureRandom = new SecureRandom(); - - /** - * 당첨자 추첨 실행 - * - * @param participants 전체 참여자 목록 - * @param winnerCount 당첨 인원 - * @param visitBonusApplied 매장 방문 가산점 적용 여부 - * @param visitBonusWeight 매장 방문 가중치 (기본 2.0) - * @return 당첨자 목록 - */ - public List executeLottery( - List participants, - int winnerCount, - boolean visitBonusApplied, - double visitBonusWeight - ) { - log.info("Starting lottery execution - Total participants: {}, Winner count: {}, Visit bonus: {}", - participants.size(), winnerCount, visitBonusApplied); - - // Step 1: 가산점 적용 (매장 방문 시) - List weightedParticipants = applyWeights(participants, visitBonusApplied, visitBonusWeight); - - // Step 2: Fisher-Yates Shuffle - List shuffled = fisherYatesShuffle(weightedParticipants); - - // Step 3: 상위 N명 선정 - List winners = shuffled.subList(0, Math.min(winnerCount, shuffled.size())); - - log.info("Lottery execution completed - Winners selected: {}", winners.size()); - return winners; - } - - /** - * Step 1: 가산점 적용 - * - 매장 방문 고객은 가중치만큼 참여자 목록에 중복 추가 - * - * @param participants 참여자 목록 - * @param visitBonusApplied 가산점 적용 여부 - * @param visitBonusWeight 가중치 - * @return 가중치 적용된 참여자 목록 - */ - private List applyWeights(List participants, boolean visitBonusApplied, double visitBonusWeight) { - if (!visitBonusApplied) { - return new ArrayList<>(participants); - } - - List weighted = new ArrayList<>(); - for (Participant p : participants) { - // 매장 방문 고객은 가중치만큼 추가 - if (p.getStoreVisited() != null && p.getStoreVisited()) { - int bonusEntries = (int) visitBonusWeight; - for (int i = 0; i < bonusEntries; i++) { - weighted.add(p); - } - } else { - // 비방문 고객은 1회 추가 - weighted.add(p); - } - } - - log.debug("Applied visit bonus - Original size: {}, Weighted size: {}", participants.size(), weighted.size()); - return weighted; - } - - /** - * Step 2: Fisher-Yates Shuffle 알고리즘 - * - 암호학적 난수 생성 (SecureRandom) - * - 시간 복잡도: O(n) - * - * @param participants 참여자 목록 - * @return 셔플된 참여자 목록 - */ - private List fisherYatesShuffle(List participants) { - List shuffled = new ArrayList<>(participants); - int n = shuffled.size(); - - // Fisher-Yates shuffle - for (int i = n - 1; i > 0; i--) { - // 암호학적으로 안전한 난수 생성 (0 ~ i 범위) - int j = secureRandom.nextInt(i + 1); - - // Swap - Participant temp = shuffled.get(i); - shuffled.set(i, shuffled.get(j)); - shuffled.set(j, temp); - } - - return shuffled; - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/service/ParticipationService.java b/participation-service/src/main/java/com/kt/event/participation/application/service/ParticipationService.java deleted file mode 100644 index 6e07e3c..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/service/ParticipationService.java +++ /dev/null @@ -1,403 +0,0 @@ -package com.kt.event.participation.application.service; - -import com.kt.event.participation.application.dto.*; -import com.kt.event.participation.common.exception.*; -import com.kt.event.participation.domain.participant.Participant; -import com.kt.event.participation.domain.participant.ParticipantRepository; -import com.kt.event.participation.infrastructure.kafka.KafkaProducerService; -import com.kt.event.participation.infrastructure.kafka.event.ParticipantRegisteredEvent; -import com.kt.event.participation.infrastructure.redis.RedisCacheService; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.time.format.DateTimeFormatter; -import java.util.List; -import java.util.Random; -import java.util.stream.Collectors; - -/** - * 이벤트 참여 서비스 - * - 참여자 등록 및 조회 - * - 중복 참여 검증 (Redis Cache + DB) - * - Kafka 이벤트 발행 - * - * 비즈니스 원칙: - * - 중복 참여 방지: 이벤트ID + 전화번호 조합으로 중복 검증 - * - 응모 번호 생성: EVT-{timestamp}-{random} 형식 - * - 캐시 우선: Redis 캐시로 성능 최적화 (Best Effort) - * - 비동기 이벤트: Kafka로 참여 이벤트 발행 (Best Effort) - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class ParticipationService { - - private final ParticipantRepository participantRepository; - private final RedisCacheService redisCacheService; - private final KafkaProducerService kafkaProducerService; - - @Value("${app.participation.draw-days-after-end:3}") - private int drawDaysAfterEnd; - - @Value("${app.participation.visit-bonus-weight:2.0}") - private double visitBonusWeight; - - @Value("${app.cache.duplicate-check-ttl:604800}") - private long duplicateCheckTtl; - - @Value("${app.cache.participant-list-ttl:600}") - private long participantListTtl; - - private static final Random random = new Random(); - - /** - * 이벤트 참여자 등록 - * - * Flow: - * 1. 중복 참여 검증 (Redis Cache → DB) - * 2. 응모 번호 생성 (EVT-{timestamp}-{random}) - * 3. 참여자 저장 - * 4. 중복 체크 캐시 저장 (TTL: 7일) - * 5. Kafka 이벤트 발행 (Best Effort) - * 6. 응답 반환 (추첨일: 이벤트 종료 + 3일) - * - * @param eventId 이벤트 ID - * @param request 참여 요청 정보 - * @return 참여 응답 (응모 번호, 참여 일시, 추첨일 등) - * @throws DuplicateParticipationException 중복 참여 시 - */ - @Transactional - public ParticipationResponse registerParticipant(String eventId, ParticipationRequest request) { - log.info("Registering participant - eventId: {}, name: {}, phone: {}", - eventId, request.getName(), maskPhoneNumber(request.getPhoneNumber())); - - // Step 1: 중복 참여 검증 (Cache → DB) - validateDuplicateParticipation(eventId, request.getPhoneNumber()); - - // Step 2: 응모 번호 생성 - String applicationNumber = generateApplicationNumber(); - - // Step 3: 참여자 엔티티 생성 및 저장 - Participant participant = Participant.builder() - .eventId(eventId) - .name(request.getName()) - .phoneNumber(request.getPhoneNumber()) - .email(request.getEmail()) - .entryPath(determineEntryPath(request)) - .applicationNumber(applicationNumber) - .participatedAt(LocalDateTime.now()) - .storeVisited(request.getStoreVisited() != null ? request.getStoreVisited() : false) - .agreeMarketing(request.getAgreeMarketing() != null ? request.getAgreeMarketing() : false) - .agreePrivacy(request.getAgreePrivacy()) - .isWinner(false) - .bonusEntries(1) - .build(); - - // 매장 방문 보너스 응모권 적용 - participant.applyVisitBonus(visitBonusWeight); - - Participant saved = participantRepository.save(participant); - - log.info("Participant registered successfully - participantId: {}, applicationNumber: {}", - saved.getParticipantId(), saved.getApplicationNumber()); - - // Step 4: 중복 체크 캐시 저장 (Best Effort) - try { - redisCacheService.cacheDuplicateCheck( - Long.parseLong(eventId), - request.getPhoneNumber(), - duplicateCheckTtl - ); - } catch (Exception e) { - log.warn("Failed to cache duplicate check - eventId: {}, phone: {}", - eventId, maskPhoneNumber(request.getPhoneNumber()), e); - } - - // Step 5: Kafka 이벤트 발행 (Best Effort) - publishParticipantRegisteredEvent(saved); - - // Step 6: 응답 반환 - return ParticipationResponse.builder() - .participantId(String.valueOf(saved.getParticipantId())) - .eventId(saved.getEventId()) - .name(saved.getName()) - .phoneNumber(saved.getPhoneNumber()) - .email(saved.getEmail()) - .participatedAt(saved.getParticipatedAt()) - .storeVisited(saved.getStoreVisited()) - .bonusEntries(saved.getBonusEntries()) - .build(); - } - - /** - * 이벤트 참여자 목록 조회 (필터링 + 페이징) - * - * Flow: - * 1. 캐시 조회 (Cache Hit → 즉시 반환) - * 2. Cache Miss → DB 조회 - * 3. 전화번호 마스킹 - * 4. 캐시 저장 (TTL: 10분) - * 5. 응답 반환 - * - * @param eventId 이벤트 ID - * @param entryPath 참여 경로 필터 (nullable) - * @param isWinner 당첨 여부 필터 (nullable) - * @param pageable 페이징 정보 - * @return 참여자 목록 (페이징) - */ - public ParticipantListResponse getParticipantList( - String eventId, - String entryPath, - Boolean isWinner, - Pageable pageable) { - - log.info("Fetching participant list - eventId: {}, entryPath: {}, isWinner: {}, page: {}", - eventId, entryPath, isWinner, pageable.getPageNumber()); - - // Step 1: 캐시 키 생성 - String cacheKey = buildCacheKey(eventId, entryPath, isWinner, pageable); - - // Step 2: 캐시 조회 (Cache Hit → 즉시 반환) - try { - var cachedData = redisCacheService.getParticipantList(cacheKey); - if (cachedData.isPresent()) { - log.debug("Cache hit - cacheKey: {}", cacheKey); - return (ParticipantListResponse) cachedData.get(); - } - } catch (Exception e) { - log.warn("Failed to retrieve from cache - cacheKey: {}", cacheKey, e); - } - - // Step 3: Cache Miss → DB 조회 - Page participantPage = participantRepository.findParticipants( - eventId, entryPath, isWinner, pageable); - - // Step 4: DTO 변환 및 전화번호 마스킹 - List participants = participantPage.getContent().stream() - .map(this::convertToDto) - .collect(Collectors.toList()); - - ParticipantListResponse response = ParticipantListResponse.builder() - .participants(participants) - .totalElements((int) participantPage.getTotalElements()) - .totalPages(participantPage.getTotalPages()) - .currentPage(participantPage.getNumber()) - .pageSize(participantPage.getSize()) - .hasNext(participantPage.hasNext()) - .hasPrevious(participantPage.hasPrevious()) - .build(); - - // Step 5: 캐시 저장 (Best Effort) - try { - redisCacheService.cacheParticipantList(cacheKey, response, participantListTtl); - } catch (Exception e) { - log.warn("Failed to cache participant list - cacheKey: {}", cacheKey, e); - } - - log.info("Participant list fetched successfully - eventId: {}, totalElements: {}", - eventId, response.getTotalElements()); - - return response; - } - - /** - * 참여자 검색 (이름 또는 전화번호) - * - * @param eventId 이벤트 ID - * @param keyword 검색 키워드 - * @param pageable 페이징 정보 - * @return 검색된 참여자 목록 (페이징) - */ - public ParticipantListResponse searchParticipants( - String eventId, - String keyword, - Pageable pageable) { - - log.info("Searching participants - eventId: {}, keyword: {}, page: {}", - eventId, keyword, pageable.getPageNumber()); - - Page participantPage = participantRepository.searchParticipants( - eventId, keyword, pageable); - - List participants = participantPage.getContent().stream() - .map(this::convertToDto) - .collect(Collectors.toList()); - - ParticipantListResponse response = ParticipantListResponse.builder() - .participants(participants) - .totalElements((int) participantPage.getTotalElements()) - .totalPages(participantPage.getTotalPages()) - .currentPage(participantPage.getNumber()) - .pageSize(participantPage.getSize()) - .hasNext(participantPage.hasNext()) - .hasPrevious(participantPage.hasPrevious()) - .build(); - - log.info("Participants searched successfully - eventId: {}, keyword: {}, totalElements: {}", - eventId, keyword, response.getTotalElements()); - - return response; - } - - // === Private Helper Methods === - - /** - * 중복 참여 검증 - * - * Flow: - * 1. Redis Cache 조회 (Cache Hit → 중복 예외) - * 2. Cache Miss → DB 조회 - * 3. DB에 존재 → 중복 예외 - * - * @param eventId 이벤트 ID - * @param phoneNumber 전화번호 - * @throws DuplicateParticipationException 중복 참여 시 - */ - private void validateDuplicateParticipation(String eventId, String phoneNumber) { - // Step 1: Cache 조회 - try { - Boolean isDuplicate = redisCacheService.checkDuplicateParticipation( - Long.parseLong(eventId), phoneNumber); - if (Boolean.TRUE.equals(isDuplicate)) { - log.warn("Duplicate participation detected from cache - eventId: {}, phone: {}", - eventId, maskPhoneNumber(phoneNumber)); - throw new DuplicateParticipationException( - String.format("이미 참여한 이벤트입니다. (이벤트: %s, 전화번호: %s)", eventId, maskPhoneNumber(phoneNumber))); - } - } catch (DuplicateParticipationException e) { - throw e; - } catch (Exception e) { - log.warn("Failed to check duplicate from cache - eventId: {}, phone: {}", - eventId, maskPhoneNumber(phoneNumber), e); - } - - // Step 2: DB 조회 - participantRepository.findByEventIdAndPhoneNumber(eventId, phoneNumber) - .ifPresent(participant -> { - log.warn("Duplicate participation detected from DB - eventId: {}, phone: {}, participantId: {}", - eventId, maskPhoneNumber(phoneNumber), participant.getParticipantId()); - throw new DuplicateParticipationException( - String.format("이미 참여한 이벤트입니다. (이벤트: %s, 전화번호: %s)", eventId, maskPhoneNumber(phoneNumber))); - }); - } - - /** - * 응모 번호 생성 - * - * 형식: EVT-{timestamp}-{random} - * 예: EVT-20250123143022-A7B9 - * - * @return 응모 번호 - */ - private String generateApplicationNumber() { - String timestamp = LocalDateTime.now() - .format(DateTimeFormatter.ofPattern("yyyyMMddHHmmss")); - String randomSuffix = String.format("%04X", random.nextInt(0x10000)); - return String.format("EVT-%s-%s", timestamp, randomSuffix); - } - - /** - * 참여 경로 결정 - * - * @param request 참여 요청 - * @return 참여 경로 - */ - private String determineEntryPath(ParticipationRequest request) { - // TODO: 실제로는 요청 헤더나 파라미터에서 참여 경로를 추출 - // 현재는 매장 방문 여부로 간단히 결정 - if (Boolean.TRUE.equals(request.getStoreVisited())) { - return "STORE_VISIT"; - } - return "WEB"; - } - - /** - * Participant 엔티티를 DTO로 변환 - * - * @param participant 참여자 엔티티 - * @return 참여자 DTO (전화번호 마스킹 처리됨) - */ - private ParticipantDto convertToDto(Participant participant) { - return ParticipantDto.builder() - .participantId(String.valueOf(participant.getParticipantId())) - .name(participant.getName()) - .maskedPhoneNumber(participant.getMaskedPhoneNumber()) - .email(participant.getEmail()) - .entryPath(participant.getEntryPath()) - .participatedAt(participant.getParticipatedAt()) - .isWinner(participant.getIsWinner()) - .wonAt(participant.getWonAt()) - .storeVisited(participant.getStoreVisited()) - .bonusEntries(participant.getBonusEntries()) - .build(); - } - - /** - * 전화번호 마스킹 - * - * @param phoneNumber 전화번호 - * @return 마스킹된 전화번호 (예: 010-****-5678) - */ - private String maskPhoneNumber(String phoneNumber) { - if (phoneNumber == null || phoneNumber.length() < 13) { - return phoneNumber; - } - return phoneNumber.substring(0, 4) + "****" + phoneNumber.substring(8); - } - - /** - * 캐시 키 생성 - * - * @param eventId 이벤트 ID - * @param entryPath 참여 경로 - * @param isWinner 당첨 여부 - * @param pageable 페이징 정보 - * @return 캐시 키 - */ - private String buildCacheKey(String eventId, String entryPath, Boolean isWinner, Pageable pageable) { - return String.format("%s:entryPath=%s:isWinner=%s:page=%d:size=%d", - eventId, - entryPath != null ? entryPath : "all", - isWinner != null ? isWinner : "all", - pageable.getPageNumber(), - pageable.getPageSize() - ); - } - - /** - * Kafka 참여자 등록 이벤트 발행 - * - * @param participant 참여자 엔티티 - */ - private void publishParticipantRegisteredEvent(Participant participant) { - try { - ParticipantRegisteredEvent event = ParticipantRegisteredEvent.builder() - .participantId(participant.getParticipantId()) - .eventId(Long.parseLong(participant.getEventId())) - .phoneNumber(participant.getPhoneNumber()) - .entryPath(participant.getEntryPath()) - .registeredAt(participant.getParticipatedAt()) - .build(); - - kafkaProducerService.publishParticipantRegistered(event); - - log.info("Participant registered event published - participantId: {}, eventId: {}", - participant.getParticipantId(), participant.getEventId()); - - } catch (Exception e) { - log.error("Failed to publish participant registered event - participantId: {}, eventId: {}", - participant.getParticipantId(), participant.getEventId(), e); - // 이벤트 발행 실패는 참여 등록 실패로 이어지지 않음 (Best Effort) - } - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/application/service/WinnerDrawService.java b/participation-service/src/main/java/com/kt/event/participation/application/service/WinnerDrawService.java deleted file mode 100644 index d4698c6..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/application/service/WinnerDrawService.java +++ /dev/null @@ -1,312 +0,0 @@ -package com.kt.event.participation.application.service; - -import com.kt.event.participation.application.dto.WinnerDrawRequest; -import com.kt.event.participation.application.dto.WinnerDrawResponse; -import com.kt.event.participation.application.dto.WinnerDto; -import com.kt.event.participation.common.exception.AlreadyDrawnException; -import com.kt.event.participation.common.exception.InsufficientParticipantsException; -import com.kt.event.participation.domain.draw.DrawLog; -import com.kt.event.participation.domain.draw.DrawLogRepository; -import com.kt.event.participation.domain.participant.Participant; -import com.kt.event.participation.domain.participant.ParticipantRepository; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; - -/** - * 당첨자 추첨 서비스 - * - 당첨자 추첨 실행 - * - 추첨 이력 관리 - * - 당첨자 조회 - * - * 비즈니스 원칙: - * - 중복 추첨 방지: 이벤트별 1회만 추첨 가능 - * - 추첨 알고리즘: Fisher-Yates Shuffle (공정성 보장) - * - 매장 방문 가산점: 설정에 따라 가중치 부여 - * - 트랜잭션 보장: 당첨자 업데이트와 추첨 로그 저장은 원자성 보장 - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@Slf4j -@Service -@RequiredArgsConstructor -@Transactional(readOnly = true) -public class WinnerDrawService { - - private final ParticipantRepository participantRepository; - private final DrawLogRepository drawLogRepository; - private final LotteryAlgorithm lotteryAlgorithm; - - @Value("${app.participation.visit-bonus-weight:2.0}") - private double visitBonusWeight; - - /** - * 당첨자 추첨 실행 - * - * Flow: - * 1. 중복 추첨 검증 (이벤트별 1회만 가능) - * 2. 미당첨 참여자 조회 - * 3. 참여자 수 검증 (당첨 인원보다 적으면 예외) - * 4. 추첨 알고리즘 실행 (Fisher-Yates Shuffle) - * 5. 당첨자 업데이트 (트랜잭션) - * 6. 추첨 로그 저장 - * 7. 응답 반환 - * - * @param eventId 이벤트 ID - * @param request 추첨 요청 정보 - * @return 추첨 결과 (당첨자 목록, 추첨 로그 ID 등) - * @throws AlreadyDrawnException 이미 추첨이 완료된 경우 - * @throws InsufficientParticipantsException 참여자가 부족한 경우 - */ - @Transactional - public WinnerDrawResponse drawWinners(String eventId, WinnerDrawRequest request) { - log.info("Starting winner draw - eventId: {}, winnerCount: {}, visitBonusApplied: {}", - eventId, request.getWinnerCount(), request.getVisitBonusApplied()); - - // Step 1: 중복 추첨 검증 - validateDuplicateDraw(eventId); - - // Step 2: 미당첨 참여자 조회 - List participants = participantRepository - .findByEventIdAndIsWinnerOrderByParticipatedAtAsc(eventId, false); - - log.info("Eligible participants for draw - eventId: {}, count: {}", eventId, participants.size()); - - // Step 3: 참여자 수 검증 - if (participants.size() < request.getWinnerCount()) { - String errorMsg = String.format( - "참여자가 부족합니다. (필요: %d명, 현재: %d명)", - request.getWinnerCount(), participants.size()); - log.error("Insufficient participants - eventId: {}, {}", eventId, errorMsg); - throw new InsufficientParticipantsException(errorMsg); - } - - // Step 4: 추첨 알고리즘 실행 - List winners; - try { - winners = lotteryAlgorithm.executeLottery( - participants, - request.getWinnerCount(), - request.getVisitBonusApplied() != null ? request.getVisitBonusApplied() : true, - visitBonusWeight - ); - - log.info("Lottery algorithm executed successfully - eventId: {}, winnersCount: {}", - eventId, winners.size()); - - } catch (Exception e) { - log.error("Failed to execute lottery algorithm - eventId: {}", eventId, e); - saveFailedDrawLog(eventId, request, participants.size(), e.getMessage()); - throw new RuntimeException("추첨 실행 중 오류가 발생했습니다: " + e.getMessage(), e); - } - - // Step 5: 당첨자 업데이트 (트랜잭션) - LocalDateTime drawnAt = LocalDateTime.now(); - winners.forEach(winner -> { - winner.markAsWinner(); - participantRepository.save(winner); - }); - - log.info("Winners marked and saved - eventId: {}, count: {}", eventId, winners.size()); - - // Step 6: 추첨 로그 저장 - DrawLog drawLog = saveSuccessDrawLog(eventId, request, participants.size(), winners.size(), drawnAt); - - // Step 7: 응답 반환 - List winnerDtos = winners.stream() - .map(this::convertToWinnerDto) - .collect(Collectors.toList()); - - WinnerDrawResponse response = WinnerDrawResponse.builder() - .winners(winnerDtos) - .drawLogId(String.valueOf(drawLog.getDrawLogId())) - .message(String.format("추첨이 완료되었습니다. 총 %d명의 당첨자가 선정되었습니다.", winners.size())) - .eventId(eventId) - .totalParticipants(participants.size()) - .winnerCount(winners.size()) - .drawnAt(drawnAt) - .build(); - - log.info("Winner draw completed successfully - eventId: {}, drawLogId: {}, winnersCount: {}", - eventId, drawLog.getDrawLogId(), winners.size()); - - return response; - } - - /** - * 당첨자 목록 조회 - * - * @param eventId 이벤트 ID - * @return 당첨자 목록 (전화번호 마스킹 처리됨) - */ - public List getWinners(String eventId) { - log.info("Fetching winners - eventId: {}", eventId); - - List winners = participantRepository - .findByEventIdAndIsWinnerOrderByWonAtDesc(eventId, true); - - List winnerDtos = winners.stream() - .map(this::convertToWinnerDto) - .collect(Collectors.toList()); - - log.info("Winners fetched successfully - eventId: {}, count: {}", eventId, winnerDtos.size()); - - return winnerDtos; - } - - // === Private Helper Methods === - - /** - * 중복 추첨 검증 - * - * 이벤트별 1회만 추첨 가능 - * 이미 추첨이 완료된 경우 예외 발생 - * - * @param eventId 이벤트 ID - * @throws AlreadyDrawnException 이미 추첨이 완료된 경우 - */ - private void validateDuplicateDraw(String eventId) { - drawLogRepository.findByEventId(eventId) - .ifPresent(drawLog -> { - if (Boolean.TRUE.equals(drawLog.getIsSuccess())) { - String errorMsg = String.format( - "이미 추첨이 완료되었습니다. (추첨일시: %s, 당첨자: %d명)", - drawLog.getDrawnAt(), drawLog.getWinnerCount()); - log.warn("Duplicate draw detected - eventId: {}, drawLogId: {}, drawnAt: {}", - eventId, drawLog.getDrawLogId(), drawLog.getDrawnAt()); - throw new AlreadyDrawnException(errorMsg); - } - }); - } - - /** - * 추첨 성공 로그 저장 - * - * @param eventId 이벤트 ID - * @param request 추첨 요청 - * @param totalParticipants 전체 참여자 수 - * @param winnerCount 당첨자 수 - * @param drawnAt 추첨 일시 - * @return 저장된 추첨 로그 - */ - private DrawLog saveSuccessDrawLog( - String eventId, - WinnerDrawRequest request, - int totalParticipants, - int winnerCount, - LocalDateTime drawnAt) { - - DrawLog drawLog = DrawLog.builder() - .eventId(eventId) - .drawMethod("RANDOM") - .algorithm("FISHER_YATES_SHUFFLE") - .visitBonusApplied(request.getVisitBonusApplied() != null ? request.getVisitBonusApplied() : true) - .winnerCount(winnerCount) - .totalParticipants(totalParticipants) - .drawnAt(drawnAt) - .drawnBy("SYSTEM") // TODO: 실제로는 인증된 사용자 ID 사용 - .isSuccess(true) - .errorMessage(null) - .settings(buildSettingsJson(request)) - .build(); - - DrawLog saved = drawLogRepository.save(drawLog); - - log.info("Draw log saved successfully - drawLogId: {}, eventId: {}, isSuccess: true", - saved.getDrawLogId(), eventId); - - return saved; - } - - /** - * 추첨 실패 로그 저장 - * - * @param eventId 이벤트 ID - * @param request 추첨 요청 - * @param totalParticipants 전체 참여자 수 - * @param errorMessage 에러 메시지 - */ - private void saveFailedDrawLog( - String eventId, - WinnerDrawRequest request, - int totalParticipants, - String errorMessage) { - - try { - DrawLog drawLog = DrawLog.builder() - .eventId(eventId) - .drawMethod("RANDOM") - .algorithm("FISHER_YATES_SHUFFLE") - .visitBonusApplied(request.getVisitBonusApplied() != null ? request.getVisitBonusApplied() : true) - .winnerCount(0) - .totalParticipants(totalParticipants) - .drawnAt(LocalDateTime.now()) - .drawnBy("SYSTEM") - .isSuccess(false) - .errorMessage(errorMessage) - .settings(buildSettingsJson(request)) - .build(); - - DrawLog saved = drawLogRepository.save(drawLog); - - log.warn("Failed draw log saved - drawLogId: {}, eventId: {}, error: {}", - saved.getDrawLogId(), eventId, errorMessage); - - } catch (Exception e) { - log.error("Failed to save failed draw log - eventId: {}", eventId, e); - } - } - - /** - * 추첨 설정 JSON 생성 - * - * @param request 추첨 요청 - * @return JSON 문자열 - */ - private String buildSettingsJson(WinnerDrawRequest request) { - return String.format( - "{\"winnerCount\":%d,\"visitBonusApplied\":%b,\"visitBonusWeight\":%.1f}", - request.getWinnerCount(), - request.getVisitBonusApplied() != null ? request.getVisitBonusApplied() : true, - visitBonusWeight - ); - } - - /** - * Participant 엔티티를 WinnerDto로 변환 - * - * @param participant 참여자 엔티티 - * @return 당첨자 DTO (전화번호 마스킹 처리됨) - */ - private WinnerDto convertToWinnerDto(Participant participant) { - return WinnerDto.builder() - .participantId(String.valueOf(participant.getParticipantId())) - .name(participant.getName()) - .maskedPhoneNumber(participant.getMaskedPhoneNumber()) - .email(participant.getEmail()) - .applicationNumber(parseApplicationNumber(participant.getApplicationNumber())) - .wonAt(participant.getWonAt()) - .storeVisited(participant.getStoreVisited()) - .bonusApplied(participant.getBonusEntries() > 1) - .build(); - } - - /** - * 응모 번호를 Integer로 파싱 - * 형식: EVT-20250123143022-A7B9 - * - * @param applicationNumber 응모 번호 문자열 - * @return 해시된 정수값 - */ - private Integer parseApplicationNumber(String applicationNumber) { - // 응모 번호 문자열을 해시하여 정수로 변환 - return applicationNumber != null ? applicationNumber.hashCode() : 0; - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/common/exception/AlreadyDrawnException.java b/participation-service/src/main/java/com/kt/event/participation/common/exception/AlreadyDrawnException.java deleted file mode 100644 index 4fb3756..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/common/exception/AlreadyDrawnException.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.kt.event.participation.common.exception; - -/** - * 이미 추첨 완료 예외 - * 이미 추첨이 완료된 이벤트에 대해 다시 추첨하려고 할 때 발생 - */ -public class AlreadyDrawnException extends ParticipationException { - - private static final String ERROR_CODE = "ALREADY_DRAWN"; - - public AlreadyDrawnException(String message) { - super(ERROR_CODE, message); - } - - public AlreadyDrawnException(String message, Throwable cause) { - super(ERROR_CODE, message, cause); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/common/exception/DuplicateParticipationException.java b/participation-service/src/main/java/com/kt/event/participation/common/exception/DuplicateParticipationException.java deleted file mode 100644 index 56bf3bb..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/common/exception/DuplicateParticipationException.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.kt.event.participation.common.exception; - -/** - * 중복 참여 예외 - * 사용자가 이미 참여한 이벤트에 다시 참여하려고 할 때 발생 - */ -public class DuplicateParticipationException extends ParticipationException { - - private static final String ERROR_CODE = "DUPLICATE_PARTICIPATION"; - - public DuplicateParticipationException(String message) { - super(ERROR_CODE, message); - } - - public DuplicateParticipationException(String message, Throwable cause) { - super(ERROR_CODE, message, cause); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/common/exception/EventNotActiveException.java b/participation-service/src/main/java/com/kt/event/participation/common/exception/EventNotActiveException.java deleted file mode 100644 index cfcf4db..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/common/exception/EventNotActiveException.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.kt.event.participation.common.exception; - -/** - * 이벤트 진행 불가 상태 예외 - * 이벤트가 ACTIVE 상태가 아니어서 참여할 수 없을 때 발생 - */ -public class EventNotActiveException extends ParticipationException { - - private static final String ERROR_CODE = "EVENT_NOT_ACTIVE"; - - public EventNotActiveException(String message) { - super(ERROR_CODE, message); - } - - public EventNotActiveException(String message, Throwable cause) { - super(ERROR_CODE, message, cause); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/common/exception/EventNotFoundException.java b/participation-service/src/main/java/com/kt/event/participation/common/exception/EventNotFoundException.java deleted file mode 100644 index 6381b8d..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/common/exception/EventNotFoundException.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.kt.event.participation.common.exception; - -/** - * 이벤트 없음 예외 - * 요청한 이벤트 ID가 존재하지 않을 때 발생 - */ -public class EventNotFoundException extends ParticipationException { - - private static final String ERROR_CODE = "EVENT_NOT_FOUND"; - - public EventNotFoundException(String message) { - super(ERROR_CODE, message); - } - - public EventNotFoundException(String message, Throwable cause) { - super(ERROR_CODE, message, cause); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/common/exception/GlobalExceptionHandler.java b/participation-service/src/main/java/com/kt/event/participation/common/exception/GlobalExceptionHandler.java deleted file mode 100644 index 9a1892a..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/common/exception/GlobalExceptionHandler.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.kt.event.participation.common.exception; - -import com.kt.event.participation.application.dto.ErrorResponse; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.validation.FieldError; -import org.springframework.web.bind.MethodArgumentNotValidException; -import org.springframework.web.bind.annotation.ExceptionHandler; -import org.springframework.web.bind.annotation.RestControllerAdvice; - -import java.util.stream.Collectors; - -/** - * 전역 예외 처리 핸들러 - * 모든 컨트롤러에서 발생하는 예외를 처리 - */ -@RestControllerAdvice -public class GlobalExceptionHandler { - - private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class); - - /** - * 중복 참여 예외 처리 - */ - @ExceptionHandler(DuplicateParticipationException.class) - public ResponseEntity handleDuplicateParticipation(DuplicateParticipationException ex) { - logger.warn("중복 참여 예외: {}", ex.getMessage()); - ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage()); - return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse); - } - - /** - * 이벤트 없음 예외 처리 - */ - @ExceptionHandler(EventNotFoundException.class) - public ResponseEntity handleEventNotFound(EventNotFoundException ex) { - logger.warn("이벤트 없음 예외: {}", ex.getMessage()); - ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage()); - return ResponseEntity.status(HttpStatus.NOT_FOUND).body(errorResponse); - } - - /** - * 이벤트 진행 불가 상태 예외 처리 - */ - @ExceptionHandler(EventNotActiveException.class) - public ResponseEntity handleEventNotActive(EventNotActiveException ex) { - logger.warn("이벤트 비활성 예외: {}", ex.getMessage()); - ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); - } - - /** - * 이미 추첨 완료 예외 처리 - */ - @ExceptionHandler(AlreadyDrawnException.class) - public ResponseEntity handleAlreadyDrawn(AlreadyDrawnException ex) { - logger.warn("이미 추첨 완료 예외: {}", ex.getMessage()); - ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage()); - return ResponseEntity.status(HttpStatus.CONFLICT).body(errorResponse); - } - - /** - * 참여자 수 부족 예외 처리 - */ - @ExceptionHandler(InsufficientParticipantsException.class) - public ResponseEntity handleInsufficientParticipants(InsufficientParticipantsException ex) { - logger.warn("참여자 수 부족 예외: {}", ex.getMessage()); - ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); - } - - /** - * 기본 커스텀 예외 처리 - */ - @ExceptionHandler(ParticipationException.class) - public ResponseEntity handleParticipationException(ParticipationException ex) { - logger.warn("참여 서비스 예외: {}", ex.getMessage()); - ErrorResponse errorResponse = new ErrorResponse(ex.getErrorCode(), ex.getMessage()); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); - } - - /** - * 유효성 검증 실패 예외 처리 - */ - @ExceptionHandler(MethodArgumentNotValidException.class) - public ResponseEntity handleValidationException(MethodArgumentNotValidException ex) { - String errorMessage = ex.getBindingResult().getFieldErrors().stream() - .map(FieldError::getDefaultMessage) - .collect(Collectors.joining(", ")); - - logger.warn("유효성 검증 실패: {}", errorMessage); - ErrorResponse errorResponse = new ErrorResponse("VALIDATION_ERROR", errorMessage); - return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse); - } - - /** - * 처리되지 않은 모든 예외 처리 - */ - @ExceptionHandler(Exception.class) - public ResponseEntity handleException(Exception ex) { - logger.error("서버 내부 오류: ", ex); - ErrorResponse errorResponse = new ErrorResponse( - "INTERNAL_SERVER_ERROR", - "서버 내부 오류가 발생했습니다." - ); - return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(errorResponse); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/common/exception/InsufficientParticipantsException.java b/participation-service/src/main/java/com/kt/event/participation/common/exception/InsufficientParticipantsException.java deleted file mode 100644 index 1ab1b7a..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/common/exception/InsufficientParticipantsException.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.kt.event.participation.common.exception; - -/** - * 참여자 수 부족 예외 - * 추첨을 진행하기에 참여자 수가 부족할 때 발생 - */ -public class InsufficientParticipantsException extends ParticipationException { - - private static final String ERROR_CODE = "INSUFFICIENT_PARTICIPANTS"; - - public InsufficientParticipantsException(String message) { - super(ERROR_CODE, message); - } - - public InsufficientParticipantsException(String message, Throwable cause) { - super(ERROR_CODE, message, cause); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/common/exception/ParticipationException.java b/participation-service/src/main/java/com/kt/event/participation/common/exception/ParticipationException.java deleted file mode 100644 index b48138e..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/common/exception/ParticipationException.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.kt.event.participation.common.exception; - -/** - * 참여 서비스 기본 예외 클래스 - * 모든 커스텀 예외의 부모 클래스 - */ -public class ParticipationException extends RuntimeException { - - private final String errorCode; - - public ParticipationException(String errorCode, String message) { - super(message); - this.errorCode = errorCode; - } - - public ParticipationException(String errorCode, String message, Throwable cause) { - super(message, cause); - this.errorCode = errorCode; - } - - public String getErrorCode() { - return errorCode; - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/domain/common/BaseEntity.java b/participation-service/src/main/java/com/kt/event/participation/domain/common/BaseEntity.java deleted file mode 100644 index 664df84..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/domain/common/BaseEntity.java +++ /dev/null @@ -1,37 +0,0 @@ -package com.kt.event.participation.domain.common; - -import jakarta.persistence.*; -import lombok.Getter; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.annotation.LastModifiedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; - -/** - * 공통 Base Entity - * - 생성일시, 수정일시 자동 관리 - * - JPA Auditing 활용 - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@Getter -@MappedSuperclass -@EntityListeners(AuditingEntityListener.class) -public abstract class BaseEntity { - - /** - * 생성일시 - */ - @CreatedDate - @Column(name = "created_at", nullable = false, updatable = false) - private LocalDateTime createdAt; - - /** - * 수정일시 - */ - @LastModifiedDate - @Column(name = "updated_at", nullable = false) - private LocalDateTime updatedAt; -} diff --git a/participation-service/src/main/java/com/kt/event/participation/domain/draw/DrawLog.java b/participation-service/src/main/java/com/kt/event/participation/domain/draw/DrawLog.java deleted file mode 100644 index cc44733..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/domain/draw/DrawLog.java +++ /dev/null @@ -1,134 +0,0 @@ -package com.kt.event.participation.domain.draw; - -import com.kt.event.participation.domain.common.BaseEntity; -import jakarta.persistence.*; -import lombok.*; - -import java.time.LocalDateTime; - -/** - * 추첨 로그 엔티티 - * - 추첨 이력 관리 - * - 감사 추적 (Audit Trail) - * - 추첨 알고리즘 및 설정 기록 - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@Entity -@Table(name = "draw_logs", - indexes = { - @Index(name = "idx_draw_log_event", columnList = "event_id"), - @Index(name = "idx_draw_log_drawn_at", columnList = "drawn_at DESC") - } -) -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder -public class DrawLog extends BaseEntity { - - /** - * 추첨 로그 ID (Primary Key) - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "draw_log_id") - private Long drawLogId; - - /** - * 이벤트 ID - */ - @Column(name = "event_id", nullable = false, length = 50) - private String eventId; - - /** - * 추첨 방법 - * - RANDOM (무작위 추첨) - * - FCFS (선착순) - */ - @Column(name = "draw_method", nullable = false, length = 20) - @Builder.Default - private String drawMethod = "RANDOM"; - - /** - * 추첨 알고리즘 - * - FISHER_YATES_SHUFFLE (Fisher-Yates 셔플) - * - CRYPTO_RANDOM (암호학적 난수 기반) - */ - @Column(name = "algorithm", nullable = false, length = 50) - @Builder.Default - private String algorithm = "FISHER_YATES_SHUFFLE"; - - /** - * 매장 방문 가산점 적용 여부 - */ - @Column(name = "visit_bonus_applied", nullable = false) - @Builder.Default - private Boolean visitBonusApplied = false; - - /** - * 당첨 인원 - */ - @Column(name = "winner_count", nullable = false) - private Integer winnerCount; - - /** - * 전체 참여자 수 (추첨 시점 기준) - */ - @Column(name = "total_participants", nullable = false) - private Integer totalParticipants; - - /** - * 추첨 일시 - */ - @Column(name = "drawn_at", nullable = false) - private LocalDateTime drawnAt; - - /** - * 추첨 실행자 (사장님 ID) - */ - @Column(name = "drawn_by", length = 50) - private String drawnBy; - - /** - * 추첨 성공 여부 - */ - @Column(name = "is_success", nullable = false) - @Builder.Default - private Boolean isSuccess = true; - - /** - * 에러 메시지 (실패 시) - */ - @Column(name = "error_message", columnDefinition = "TEXT") - private String errorMessage; - - /** - * 추첨 설정 (JSON) - * - 추가 설정 정보 저장 - */ - @Column(name = "settings", columnDefinition = "TEXT") - private String settings; - - // === Business Methods === - - /** - * 추첨 실패 처리 - * @param errorMessage 에러 메시지 - */ - public void markAsFailed(String errorMessage) { - this.isSuccess = false; - this.errorMessage = errorMessage; - } - - /** - * 추첨 성공 처리 - * @param winnerCount 당첨 인원 - */ - public void markAsSuccess(int winnerCount) { - this.isSuccess = true; - this.winnerCount = winnerCount; - this.drawnAt = LocalDateTime.now(); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/domain/draw/DrawLogRepository.java b/participation-service/src/main/java/com/kt/event/participation/domain/draw/DrawLogRepository.java deleted file mode 100644 index d3a66e9..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/domain/draw/DrawLogRepository.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.kt.event.participation.domain.draw; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -/** - * 추첨 로그 Repository - * - 추첨 이력 CRUD - * - 중복 추첨 검증 - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@Repository -public interface DrawLogRepository extends JpaRepository { - - /** - * 이벤트별 추첨 로그 조회 - * - 중복 추첨 방지를 위해 사용 - * - * @param eventId 이벤트 ID - * @return 추첨 로그 (존재하지 않으면 empty) - */ - Optional findByEventId(String eventId); - - /** - * 이벤트별 추첨 이력 전체 조회 - * - 추첨 일시 내림차순 정렬 - * - * @param eventId 이벤트 ID - * @return 추첨 로그 목록 - */ - List findByEventIdOrderByDrawnAtDesc(String eventId); - - /** - * 성공한 추첨 이력 조회 - * - * @param eventId 이벤트 ID - * @param isSuccess 성공 여부 (true) - * @return 추첨 로그 목록 - */ - List findByEventIdAndIsSuccessOrderByDrawnAtDesc(String eventId, Boolean isSuccess); -} diff --git a/participation-service/src/main/java/com/kt/event/participation/domain/participant/Participant.java b/participation-service/src/main/java/com/kt/event/participation/domain/participant/Participant.java deleted file mode 100644 index fa328e8..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/domain/participant/Participant.java +++ /dev/null @@ -1,161 +0,0 @@ -package com.kt.event.participation.domain.participant; - -import com.kt.event.participation.domain.common.BaseEntity; -import jakarta.persistence.*; -import lombok.*; - -import java.time.LocalDateTime; - -/** - * 이벤트 참여자 엔티티 - * - 이벤트 참여 정보 관리 - * - 중복 참여 방지 (eventId + phoneNumber 복합 유니크) - * - 당첨자 정보 포함 - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@Entity -@Table(name = "participants", - uniqueConstraints = { - @UniqueConstraint(name = "uk_participant_event_phone", columnNames = {"event_id", "phone_number"}) - }, - indexes = { - @Index(name = "idx_participant_event_filters", columnList = "event_id, entry_path, is_winner, participated_at DESC"), - @Index(name = "idx_participant_phone", columnList = "phone_number"), - @Index(name = "idx_participant_event_winner", columnList = "event_id, is_winner") - } -) -@Getter -@NoArgsConstructor(access = AccessLevel.PROTECTED) -@AllArgsConstructor -@Builder -public class Participant extends BaseEntity { - - /** - * 참여자 ID (Primary Key) - */ - @Id - @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "participant_id") - private Long participantId; - - /** - * 이벤트 ID (외래키는 다른 서비스이므로 논리적 연관만) - */ - @Column(name = "event_id", nullable = false, length = 50) - private String eventId; - - /** - * 참여자 이름 - */ - @Column(name = "name", nullable = false, length = 100) - private String name; - - /** - * 전화번호 (중복 참여 방지 키) - */ - @Column(name = "phone_number", nullable = false, length = 20) - private String phoneNumber; - - /** - * 이메일 - */ - @Column(name = "email", length = 255) - private String email; - - /** - * 참여 경로 - * - SNS, STORE_VISIT, BLOG, TV, etc. - */ - @Column(name = "entry_path", nullable = false, length = 50) - private String entryPath; - - /** - * 응모 번호 - * - 형식: EVT-{timestamp}-{random} - */ - @Column(name = "application_number", nullable = false, unique = true, length = 50) - private String applicationNumber; - - /** - * 참여 일시 - */ - @Column(name = "participated_at", nullable = false) - private LocalDateTime participatedAt; - - /** - * 매장 방문 여부 - */ - @Column(name = "store_visited", nullable = false) - @Builder.Default - private Boolean storeVisited = false; - - /** - * 마케팅 수신 동의 - */ - @Column(name = "agree_marketing", nullable = false) - @Builder.Default - private Boolean agreeMarketing = false; - - /** - * 개인정보 수집/이용 동의 - */ - @Column(name = "agree_privacy", nullable = false) - @Builder.Default - private Boolean agreePrivacy = true; - - /** - * 당첨 여부 - */ - @Column(name = "is_winner", nullable = false) - @Builder.Default - private Boolean isWinner = false; - - /** - * 당첨 일시 - */ - @Column(name = "won_at") - private LocalDateTime wonAt; - - /** - * 보너스 응모권 수 - * - 매장 방문 시 추가 응모권 부여 - */ - @Column(name = "bonus_entries", nullable = false) - @Builder.Default - private Integer bonusEntries = 1; - - // === Business Methods === - - /** - * 당첨자로 변경 - */ - public void markAsWinner() { - this.isWinner = true; - this.wonAt = LocalDateTime.now(); - } - - /** - * 매장 방문 여부에 따라 보너스 응모권 부여 - * @param visitBonusWeight 매장 방문 가중치 (기본 2.0) - */ - public void applyVisitBonus(double visitBonusWeight) { - if (this.storeVisited != null && this.storeVisited) { - this.bonusEntries = (int) visitBonusWeight; - } else { - this.bonusEntries = 1; - } - } - - /** - * 전화번호 마스킹 - * @return 마스킹된 전화번호 (예: 010-****-5678) - */ - public String getMaskedPhoneNumber() { - if (phoneNumber == null || phoneNumber.length() < 13) { - return phoneNumber; - } - return phoneNumber.substring(0, 4) + "****" + phoneNumber.substring(8); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/domain/participant/ParticipantRepository.java b/participation-service/src/main/java/com/kt/event/participation/domain/participant/ParticipantRepository.java deleted file mode 100644 index 17e085c..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/domain/participant/ParticipantRepository.java +++ /dev/null @@ -1,132 +0,0 @@ -package com.kt.event.participation.domain.participant; - -import org.springframework.data.domain.Page; -import org.springframework.data.domain.Pageable; -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -import java.util.List; -import java.util.Optional; - -/** - * 참여자 Repository - * - 참여자 CRUD 및 조회 기능 - * - 중복 참여 검증 - * - 당첨자 관리 - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@Repository -public interface ParticipantRepository extends JpaRepository { - - /** - * 중복 참여 검증 - * - 이벤트ID + 전화번호로 중복 참여 확인 - * - * @param eventId 이벤트 ID - * @param phoneNumber 전화번호 - * @return 참여자 정보 (존재하지 않으면 empty) - */ - Optional findByEventIdAndPhoneNumber(String eventId, String phoneNumber); - - /** - * 이벤트별 참여자 목록 조회 (페이징) - * - 참여일시 내림차순 정렬 - * - * @param eventId 이벤트 ID - * @param pageable 페이징 정보 - * @return 참여자 목록 (페이징) - */ - Page findByEventIdOrderByParticipatedAtDesc(String eventId, Pageable pageable); - - /** - * 이벤트별 참여자 목록 조회 (필터링 + 페이징) - * - 참여 경로 필터 - * - 당첨 여부 필터 - * - 참여일시 내림차순 정렬 - * - * @param eventId 이벤트 ID - * @param entryPath 참여 경로 (nullable) - * @param isWinner 당첨 여부 (nullable) - * @param pageable 페이징 정보 - * @return 참여자 목록 (페이징) - */ - @Query("SELECT p FROM Participant p WHERE p.eventId = :eventId " + - "AND (:entryPath IS NULL OR p.entryPath = :entryPath) " + - "AND (:isWinner IS NULL OR p.isWinner = :isWinner) " + - "ORDER BY p.participatedAt DESC") - Page findParticipants( - @Param("eventId") String eventId, - @Param("entryPath") String entryPath, - @Param("isWinner") Boolean isWinner, - Pageable pageable - ); - - /** - * 이벤트별 참여자 검색 (이름 또는 전화번호) - * - LIKE 검색 지원 - * - 참여일시 내림차순 정렬 - * - * @param eventId 이벤트 ID - * @param searchKeyword 검색 키워드 - * @param pageable 페이징 정보 - * @return 참여자 목록 (페이징) - */ - @Query("SELECT p FROM Participant p WHERE p.eventId = :eventId " + - "AND (p.name LIKE %:searchKeyword% OR p.phoneNumber LIKE %:searchKeyword%) " + - "ORDER BY p.participatedAt DESC") - Page searchParticipants( - @Param("eventId") String eventId, - @Param("searchKeyword") String searchKeyword, - Pageable pageable - ); - - /** - * 이벤트별 미당첨 참여자 전체 조회 - * - 추첨 알고리즘에서 사용 - * - 참여일시 오름차순 정렬 (공정성) - * - * @param eventId 이벤트 ID - * @param isWinner 당첨 여부 (false) - * @return 미당첨 참여자 전체 목록 - */ - List findByEventIdAndIsWinnerOrderByParticipatedAtAsc(String eventId, Boolean isWinner); - - /** - * 이벤트별 당첨자 목록 조회 - * - 당첨 일시 내림차순 정렬 - * - * @param eventId 이벤트 ID - * @param isWinner 당첨 여부 (true) - * @return 당첨자 목록 - */ - List findByEventIdAndIsWinnerOrderByWonAtDesc(String eventId, Boolean isWinner); - - /** - * 이벤트별 전체 참여자 수 조회 - * - * @param eventId 이벤트 ID - * @return 전체 참여자 수 - */ - Long countByEventId(String eventId); - - /** - * 이벤트별 당첨자 수 조회 - * - * @param eventId 이벤트 ID - * @param isWinner 당첨 여부 (true) - * @return 당첨자 수 - */ - Long countByEventIdAndIsWinner(String eventId, Boolean isWinner); - - /** - * 응모 번호로 참여자 조회 - * - * @param applicationNumber 응모 번호 - * @return 참여자 정보 (존재하지 않으면 empty) - */ - Optional findByApplicationNumber(String applicationNumber); -} diff --git a/participation-service/src/main/java/com/kt/event/participation/infrastructure/kafka/KafkaProducerService.java b/participation-service/src/main/java/com/kt/event/participation/infrastructure/kafka/KafkaProducerService.java deleted file mode 100644 index b69bee6..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/infrastructure/kafka/KafkaProducerService.java +++ /dev/null @@ -1,59 +0,0 @@ -package com.kt.event.participation.infrastructure.kafka; - -import com.kt.event.participation.infrastructure.kafka.event.ParticipantRegisteredEvent; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.kafka.core.KafkaTemplate; -import org.springframework.kafka.support.SendResult; -import org.springframework.stereotype.Service; - -import java.util.concurrent.CompletableFuture; - -/** - * Kafka Producer 서비스 - * - * 참가자 등록 이벤트를 Kafka 토픽에 발행 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class KafkaProducerService { - - private final KafkaTemplate kafkaTemplate; - - @Value("${spring.kafka.topics.participant-registered}") - private String participantTopic; - - /** - * 참가자 등록 이벤트 발행 - * - * @param event 참가자 등록 이벤트 - */ - public void publishParticipantRegistered(ParticipantRegisteredEvent event) { - try { - log.info("Publishing participant registered event: eventId={}, participantId={}", - event.getEventId(), event.getParticipantId()); - - // Kafka 메시지 전송 (비동기) - CompletableFuture> future = - kafkaTemplate.send(participantTopic, String.valueOf(event.getEventId()), event); - - // 전송 결과 처리 - future.whenComplete((result, ex) -> { - if (ex == null) { - log.info("Participant registered event published successfully: eventId={}, participantId={}, offset={}", - event.getEventId(), event.getParticipantId(), result.getRecordMetadata().offset()); - } else { - log.error("Failed to publish participant registered event: eventId={}, participantId={}", - event.getEventId(), event.getParticipantId(), ex); - } - }); - - } catch (Exception e) { - log.error("Error publishing participant registered event: eventId={}, participantId={}", - event.getEventId(), event.getParticipantId(), e); - // 이벤트 발행 실패는 참가자 등록 실패로 이어지지 않음 (Best Effort) - } - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/infrastructure/kafka/config/KafkaConfig.java b/participation-service/src/main/java/com/kt/event/participation/infrastructure/kafka/config/KafkaConfig.java deleted file mode 100644 index cba5ede..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/infrastructure/kafka/config/KafkaConfig.java +++ /dev/null @@ -1,22 +0,0 @@ -package com.kt.event.participation.infrastructure.kafka.config; - -import org.springframework.context.annotation.Configuration; -import org.springframework.kafka.annotation.EnableKafka; - -/** - * Kafka 설정 - * - * Spring Boot의 Auto Configuration을 사용하므로 별도 Bean 설정 불필요 - * application.yml에서 설정 관리: - * - spring.kafka.bootstrap-servers - * - spring.kafka.producer.key-serializer - * - spring.kafka.producer.value-serializer - * - spring.kafka.producer.acks - * - spring.kafka.producer.retries - */ -@EnableKafka -@Configuration -public class KafkaConfig { - // Spring Boot Auto Configuration 사용 - // 필요 시 KafkaTemplate Bean 커스터마이징 가능 -} diff --git a/participation-service/src/main/java/com/kt/event/participation/infrastructure/kafka/event/ParticipantRegisteredEvent.java b/participation-service/src/main/java/com/kt/event/participation/infrastructure/kafka/event/ParticipantRegisteredEvent.java deleted file mode 100644 index e340d76..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/infrastructure/kafka/event/ParticipantRegisteredEvent.java +++ /dev/null @@ -1,46 +0,0 @@ -package com.kt.event.participation.infrastructure.kafka.event; - -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Data; -import lombok.NoArgsConstructor; - -import java.time.LocalDateTime; - -/** - * Kafka Event: 참가자 등록 이벤트 - * - * 참가자가 이벤트에 등록되었을 때 발행되는 이벤트 - * 이벤트 알림 서비스 등에서 소비하여 SMS/카카오톡 발송 등의 작업 수행 - */ -@Data -@Builder -@NoArgsConstructor -@AllArgsConstructor -public class ParticipantRegisteredEvent { - - /** - * 참가자 ID (PK) - */ - private Long participantId; - - /** - * 이벤트 ID - */ - private Long eventId; - - /** - * 참가자 전화번호 - */ - private String phoneNumber; - - /** - * 유입 경로 (QR, LINK, DIRECT 등) - */ - private String entryPath; - - /** - * 등록 일시 - */ - private LocalDateTime registeredAt; -} diff --git a/participation-service/src/main/java/com/kt/event/participation/infrastructure/redis/RedisCacheService.java b/participation-service/src/main/java/com/kt/event/participation/infrastructure/redis/RedisCacheService.java deleted file mode 100644 index 026a5a9..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/infrastructure/redis/RedisCacheService.java +++ /dev/null @@ -1,166 +0,0 @@ -package com.kt.event.participation.infrastructure.redis; - -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.stereotype.Service; - -import java.util.Optional; -import java.util.concurrent.TimeUnit; - -/** - * Redis 캐시 서비스 - * - * 중복 참여 체크 및 참가자 목록 캐싱 기능 제공 - */ -@Slf4j -@Service -@RequiredArgsConstructor -public class RedisCacheService { - - private final RedisTemplate redisTemplate; - - @Value("${app.cache.duplicate-check-ttl:604800}") // 기본 7일 - private long duplicateCheckTtl; - - @Value("${app.cache.participant-list-ttl:600}") // 기본 10분 - private long participantListTtl; - - private static final String DUPLICATE_CHECK_PREFIX = "duplicate:"; - private static final String PARTICIPANT_LIST_PREFIX = "participants:"; - - /** - * 중복 참여 여부 확인 - * - * @param eventId 이벤트 ID - * @param phoneNumber 전화번호 - * @return 중복 참여 여부 (true: 중복, false: 중복 아님) - */ - public Boolean checkDuplicateParticipation(Long eventId, String phoneNumber) { - try { - String key = buildDuplicateCheckKey(eventId, phoneNumber); - Boolean exists = redisTemplate.hasKey(key); - - log.debug("Duplicate participation check: eventId={}, phoneNumber={}, isDuplicate={}", - eventId, phoneNumber, exists); - - return Boolean.TRUE.equals(exists); - - } catch (Exception e) { - log.error("Error checking duplicate participation: eventId={}, phoneNumber={}", - eventId, phoneNumber, e); - // Redis 장애 시 중복 체크 실패로 처리하지 않음 (DB에서 확인) - return false; - } - } - - /** - * 중복 참여 정보 캐싱 - * - * @param eventId 이벤트 ID - * @param phoneNumber 전화번호 - * @param ttl TTL (초 단위, null일 경우 기본값 사용) - */ - public void cacheDuplicateCheck(Long eventId, String phoneNumber, Long ttl) { - try { - String key = buildDuplicateCheckKey(eventId, phoneNumber); - long effectiveTtl = (ttl != null) ? ttl : duplicateCheckTtl; - - redisTemplate.opsForValue().set(key, "1", effectiveTtl, TimeUnit.SECONDS); - - log.debug("Cached duplicate check: eventId={}, phoneNumber={}, ttl={}", - eventId, phoneNumber, effectiveTtl); - - } catch (Exception e) { - log.error("Error caching duplicate check: eventId={}, phoneNumber={}", - eventId, phoneNumber, e); - // 캐싱 실패는 중요하지 않음 (Best Effort) - } - } - - /** - * 참가자 목록 캐싱 - * - * @param key 캐시 키 - * @param data 캐싱할 데이터 - * @param ttl TTL (초 단위, null일 경우 기본값 사용) - */ - public void cacheParticipantList(String key, Object data, Long ttl) { - try { - String cacheKey = buildParticipantListKey(key); - long effectiveTtl = (ttl != null) ? ttl : participantListTtl; - - redisTemplate.opsForValue().set(cacheKey, data, effectiveTtl, TimeUnit.SECONDS); - - log.debug("Cached participant list: key={}, ttl={}", cacheKey, effectiveTtl); - - } catch (Exception e) { - log.error("Error caching participant list: key={}", key, e); - // 캐싱 실패는 중요하지 않음 (Best Effort) - } - } - - /** - * 참가자 목록 조회 - * - * @param key 캐시 키 - * @return 캐싱된 데이터 (Optional) - */ - public Optional getParticipantList(String key) { - try { - String cacheKey = buildParticipantListKey(key); - Object data = redisTemplate.opsForValue().get(cacheKey); - - log.debug("Retrieved participant list from cache: key={}, found={}", - cacheKey, data != null); - - return Optional.ofNullable(data); - - } catch (Exception e) { - log.error("Error retrieving participant list from cache: key={}", key, e); - return Optional.empty(); - } - } - - /** - * 참가자 목록 캐시 무효화 - * - * @param eventId 이벤트 ID - */ - public void invalidateParticipantListCache(Long eventId) { - try { - // 이벤트 ID로 시작하는 모든 참가자 목록 캐시 삭제 - String pattern = buildParticipantListKey(eventId + "*"); - redisTemplate.keys(pattern).forEach(key -> { - redisTemplate.delete(key); - log.debug("Invalidated participant list cache: key={}", key); - }); - - } catch (Exception e) { - log.error("Error invalidating participant list cache: eventId={}", eventId, e); - // 캐시 무효화 실패는 중요하지 않음 (Best Effort) - } - } - - /** - * 중복 체크 캐시 키 생성 - * - * @param eventId 이벤트 ID - * @param phoneNumber 전화번호 - * @return 캐시 키 - */ - private String buildDuplicateCheckKey(Long eventId, String phoneNumber) { - return DUPLICATE_CHECK_PREFIX + eventId + ":" + phoneNumber; - } - - /** - * 참가자 목록 캐시 키 생성 - * - * @param key 키 - * @return 캐시 키 - */ - private String buildParticipantListKey(String key) { - return PARTICIPANT_LIST_PREFIX + key; - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/infrastructure/redis/config/RedisConfig.java b/participation-service/src/main/java/com/kt/event/participation/infrastructure/redis/config/RedisConfig.java deleted file mode 100644 index 3e109c0..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/infrastructure/redis/config/RedisConfig.java +++ /dev/null @@ -1,104 +0,0 @@ -package com.kt.event.participation.infrastructure.redis.config; - -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.SerializationFeature; -import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; -import org.springframework.beans.factory.annotation.Value; -import org.springframework.cache.CacheManager; -import org.springframework.cache.annotation.EnableCaching; -import org.springframework.context.annotation.Bean; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.cache.RedisCacheConfiguration; -import org.springframework.data.redis.cache.RedisCacheManager; -import org.springframework.data.redis.connection.RedisConnectionFactory; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer; -import org.springframework.data.redis.serializer.RedisSerializationContext; -import org.springframework.data.redis.serializer.StringRedisSerializer; - -import java.time.Duration; - -/** - * Redis 설정 - * - * Redis 캐시 및 RedisTemplate 설정 - */ -@Configuration -@EnableCaching -public class RedisConfig { - - @Value("${app.cache.duplicate-check-ttl:604800}") // 기본 7일 - private long duplicateCheckTtl; - - @Value("${app.cache.participant-list-ttl:600}") // 기본 10분 - private long participantListTtl; - - /** - * RedisTemplate 설정 - * - * @param connectionFactory Redis 연결 팩토리 - * @return RedisTemplate - */ - @Bean - public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { - RedisTemplate template = new RedisTemplate<>(); - template.setConnectionFactory(connectionFactory); - - // Key Serializer: String - StringRedisSerializer stringSerializer = new StringRedisSerializer(); - template.setKeySerializer(stringSerializer); - template.setHashKeySerializer(stringSerializer); - - // Value Serializer: JSON - GenericJackson2JsonRedisSerializer jsonSerializer = createJsonSerializer(); - template.setValueSerializer(jsonSerializer); - template.setHashValueSerializer(jsonSerializer); - - template.afterPropertiesSet(); - return template; - } - - /** - * CacheManager 설정 - * - * @param connectionFactory Redis 연결 팩토리 - * @return CacheManager - */ - @Bean - public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { - // 기본 캐시 설정 (participant-list 용) - RedisCacheConfiguration defaultConfig = RedisCacheConfiguration.defaultCacheConfig() - .entryTtl(Duration.ofSeconds(participantListTtl)) - .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer( - new StringRedisSerializer())) - .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer( - createJsonSerializer())); - - return RedisCacheManager.builder(connectionFactory) - .cacheDefaults(defaultConfig) - .build(); - } - - /** - * JSON Serializer 생성 - * - * @return GenericJackson2JsonRedisSerializer - */ - private GenericJackson2JsonRedisSerializer createJsonSerializer() { - ObjectMapper objectMapper = new ObjectMapper(); - - // Java 8 날짜/시간 타입 지원 - objectMapper.registerModule(new JavaTimeModule()); - objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - - // 타입 정보 포함 (역직렬화 시 타입 안전성 보장) - objectMapper.activateDefaultTyping( - objectMapper.getPolymorphicTypeValidator(), - ObjectMapper.DefaultTyping.NON_FINAL, - JsonTypeInfo.As.PROPERTY - ); - - return new GenericJackson2JsonRedisSerializer(objectMapper); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/presentation/controller/ParticipationController.java b/participation-service/src/main/java/com/kt/event/participation/presentation/controller/ParticipationController.java deleted file mode 100644 index 5a9b95e..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/presentation/controller/ParticipationController.java +++ /dev/null @@ -1,181 +0,0 @@ -package com.kt.event.participation.presentation.controller; - -import com.kt.event.participation.application.dto.ParticipantListResponse; -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 jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; -import org.springframework.http.HttpStatus; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -/** - * 이벤트 참여 컨트롤러 - * - 이벤트 참여 등록 - * - 참여자 목록 조회 (필터링 + 페이징) - * - 참여자 검색 - * - * RESTful API 설계 원칙: - * - Base Path: /events/{eventId} - * - HTTP Method 사용: POST (등록), GET (조회) - * - HTTP Status Code: 201 (생성), 200 (조회), 400 (잘못된 요청), 404 (찾을 수 없음) - * - Request Validation: @Valid 사용하여 요청 검증 - * - Error Handling: GlobalExceptionHandler에서 처리 - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@Slf4j -@RestController -@RequestMapping("/events/{eventId}") -@RequiredArgsConstructor -public class ParticipationController { - - private final ParticipationService participationService; - - /** - * 이벤트 참여 - * - *

고객이 이벤트에 참여합니다.

- * - *

비즈니스 로직:

- *
    - *
  • 중복 참여 검증 (전화번호 기반)
  • - *
  • 이벤트 진행 상태 검증
  • - *
  • 응모 번호 생성 (EVT-{timestamp}-{random})
  • - *
  • Kafka 이벤트 발행 (ParticipantRegistered)
  • - *
- * - *

Response:

- *
    - *
  • 201 Created: 참여 성공
  • - *
  • 400 Bad Request: 유효하지 않은 요청, 중복 참여
  • - *
  • 404 Not Found: 이벤트를 찾을 수 없음
  • - *
  • 409 Conflict: 이벤트 진행 불가 상태
  • - *
- * - * @param eventId 이벤트 ID (Path Variable) - * @param request 참여 요청 정보 (이름, 전화번호, 이메일, 개인정보 동의 등) - * @return 참여 응답 (참여자 ID, 응모 번호, 참여 일시 등) - */ - @PostMapping("/participate") - public ResponseEntity participateEvent( - @PathVariable("eventId") String eventId, - @Valid @RequestBody ParticipationRequest request) { - - log.info("POST /events/{}/participate - name: {}, storeVisited: {}", - eventId, request.getName(), request.getStoreVisited()); - - ParticipationResponse response = participationService.registerParticipant(eventId, request); - - log.info("Event participation successful - eventId: {}, participantId: {}", - eventId, response.getParticipantId()); - - return ResponseEntity.status(HttpStatus.CREATED).body(response); - } - - /** - * 참여자 목록 조회 - * - *

이벤트의 참여자 목록을 조회합니다.

- * - *

기능:

- *
    - *
  • 페이징 지원 (기본: page=0, size=20)
  • - *
  • 참여일시 기준 정렬 (최신순)
  • - *
  • 매장 방문 여부 필터링 (선택)
  • - *
  • 당첨 여부 필터링 (선택)
  • - *
  • 전화번호 마스킹 처리 (개인정보 보호)
  • - *
- * - *

Response:

- *
    - *
  • 200 OK: 조회 성공
  • - *
  • 404 Not Found: 이벤트를 찾을 수 없음
  • - *
- * - * @param eventId 이벤트 ID (Path Variable) - * @param page 페이지 번호 (0부터 시작, 기본값: 0) - * @param size 페이지 크기 (기본값: 20, 최대: 100) - * @param storeVisited 매장 방문 여부 필터 (nullable) - * @param isWinner 당첨 여부 필터 (nullable) - * @return 참여자 목록 (페이징 정보 포함) - */ - @GetMapping("/participants") - public ResponseEntity getParticipants( - @PathVariable("eventId") String eventId, - @RequestParam(value = "page", defaultValue = "0") int page, - @RequestParam(value = "size", defaultValue = "20") int size, - @RequestParam(value = "storeVisited", required = false) Boolean storeVisited, - @RequestParam(value = "isWinner", required = false) Boolean isWinner) { - - log.info("GET /events/{}/participants - page: {}, size: {}, storeVisited: {}, isWinner: {}", - eventId, page, size, storeVisited, isWinner); - - // 페이지 크기 제한 (최대 100) - int validatedSize = Math.min(size, 100); - - Pageable pageable = PageRequest.of(page, validatedSize); - - // 참여 경로는 API 스펙에 없으므로 null로 전달 - ParticipantListResponse response = participationService.getParticipantList( - eventId, null, isWinner, pageable); - - log.info("Participant list fetched successfully - eventId: {}, totalElements: {}", - eventId, response.getTotalElements()); - - return ResponseEntity.ok(response); - } - - /** - * 참여자 검색 - * - *

이벤트의 참여자를 이름 또는 전화번호로 검색합니다.

- * - *

기능:

- *
    - *
  • 이름 또는 전화번호로 검색 (부분 일치)
  • - *
  • 페이징 지원 (기본: page=0, size=20)
  • - *
  • 전화번호 마스킹 처리 (개인정보 보호)
  • - *
- * - *

Response:

- *
    - *
  • 200 OK: 검색 성공
  • - *
  • 404 Not Found: 이벤트를 찾을 수 없음
  • - *
- * - * @param eventId 이벤트 ID (Path Variable) - * @param keyword 검색 키워드 (이름 또는 전화번호) - * @param page 페이지 번호 (0부터 시작, 기본값: 0) - * @param size 페이지 크기 (기본값: 20, 최대: 100) - * @return 검색된 참여자 목록 (페이징 정보 포함) - */ - @GetMapping("/participants/search") - public ResponseEntity searchParticipants( - @PathVariable("eventId") String eventId, - @RequestParam("keyword") String keyword, - @RequestParam(value = "page", defaultValue = "0") int page, - @RequestParam(value = "size", defaultValue = "20") int size) { - - log.info("GET /events/{}/participants/search - keyword: {}, page: {}, size: {}", - eventId, keyword, page, size); - - // 페이지 크기 제한 (최대 100) - int validatedSize = Math.min(size, 100); - - Pageable pageable = PageRequest.of(page, validatedSize); - - ParticipantListResponse response = participationService.searchParticipants( - eventId, keyword, pageable); - - log.info("Participants searched successfully - eventId: {}, keyword: {}, totalElements: {}", - eventId, keyword, response.getTotalElements()); - - return ResponseEntity.ok(response); - } -} diff --git a/participation-service/src/main/java/com/kt/event/participation/presentation/controller/WinnerController.java b/participation-service/src/main/java/com/kt/event/participation/presentation/controller/WinnerController.java deleted file mode 100644 index 6de791c..0000000 --- a/participation-service/src/main/java/com/kt/event/participation/presentation/controller/WinnerController.java +++ /dev/null @@ -1,114 +0,0 @@ -package com.kt.event.participation.presentation.controller; - -import com.kt.event.participation.application.dto.WinnerDrawRequest; -import com.kt.event.participation.application.dto.WinnerDrawResponse; -import com.kt.event.participation.application.dto.WinnerDto; -import com.kt.event.participation.application.service.WinnerDrawService; -import jakarta.validation.Valid; -import lombok.RequiredArgsConstructor; -import lombok.extern.slf4j.Slf4j; -import org.springframework.http.ResponseEntity; -import org.springframework.web.bind.annotation.*; - -import java.util.List; - -/** - * 당첨자 추첨 및 관리 컨트롤러 - * - 당첨자 추첨 실행 - * - 당첨자 목록 조회 - * - * RESTful API 설계 원칙: - * - Base Path: /events/{eventId} - * - HTTP Method 사용: POST (추첨), GET (조회) - * - HTTP Status Code: 200 (성공), 400 (잘못된 요청), 404 (찾을 수 없음), 409 (중복 추첨) - * - Request Validation: @Valid 사용하여 요청 검증 - * - Error Handling: GlobalExceptionHandler에서 처리 - * - * @author Digital Garage Team - * @since 2025-10-23 - */ -@Slf4j -@RestController -@RequestMapping("/events/{eventId}") -@RequiredArgsConstructor -public class WinnerController { - - private final WinnerDrawService winnerDrawService; - - /** - * 당첨자 추첨 - * - *

이벤트 당첨자를 추첨합니다.

- * - *

비즈니스 로직:

- *
    - *
  • 중복 추첨 검증 (이벤트별 1회만 가능)
  • - *
  • 참여자 수 검증 (당첨자 수보다 많아야 함)
  • - *
  • Fisher-Yates Shuffle 알고리즘 사용
  • - *
  • 매장 방문 보너스 가중치 적용 (선택)
  • - *
  • 추첨 로그 저장 (감사 추적)
  • - *
- * - *

Response:

- *
    - *
  • 200 OK: 추첨 성공
  • - *
  • 400 Bad Request: 유효하지 않은 요청 (당첨자 수가 참여자 수보다 많음)
  • - *
  • 404 Not Found: 이벤트를 찾을 수 없음
  • - *
  • 409 Conflict: 이미 추첨 완료
  • - *
- * - * @param eventId 이벤트 ID (Path Variable) - * @param request 추첨 요청 정보 (당첨자 수, 매장 방문 보너스 적용 여부) - * @return 추첨 결과 (당첨자 목록, 추첨 일시, 추첨 로그 ID 등) - */ - @PostMapping("/draw-winners") - public ResponseEntity drawWinners( - @PathVariable("eventId") String eventId, - @Valid @RequestBody WinnerDrawRequest request) { - - log.info("POST /events/{}/draw-winners - winnerCount: {}, visitBonusApplied: {}", - eventId, request.getWinnerCount(), request.getVisitBonusApplied()); - - WinnerDrawResponse response = winnerDrawService.drawWinners(eventId, request); - - log.info("Winners drawn successfully - eventId: {}, drawLogId: {}, winnerCount: {}", - eventId, response.getDrawLogId(), response.getWinnerCount()); - - return ResponseEntity.ok(response); - } - - /** - * 당첨자 목록 조회 - * - *

이벤트의 당첨자 목록을 조회합니다.

- * - *

기능:

- *
    - *
  • 당첨 순위별 정렬 (당첨 일시 내림차순)
  • - *
  • 전화번호 마스킹 처리 (개인정보 보호)
  • - *
  • 응모 번호 포함
  • - *
- * - *

Response:

- *
    - *
  • 200 OK: 조회 성공
  • - *
  • 404 Not Found: 이벤트를 찾을 수 없음 또는 당첨자가 없음
  • - *
- * - * @param eventId 이벤트 ID (Path Variable) - * @return 당첨자 목록 (당첨 순위, 이름, 마스킹된 전화번호, 당첨 일시 등) - */ - @GetMapping("/winners") - public ResponseEntity> getWinners( - @PathVariable("eventId") String eventId) { - - log.info("GET /events/{}/winners", eventId); - - List winners = winnerDrawService.getWinners(eventId); - - log.info("Winners fetched successfully - eventId: {}, count: {}", - eventId, winners.size()); - - return ResponseEntity.ok(winners); - } -} diff --git a/participation-service/src/main/resources/application.yml b/participation-service/src/main/resources/application.yml deleted file mode 100644 index 7fc673d..0000000 --- a/participation-service/src/main/resources/application.yml +++ /dev/null @@ -1,88 +0,0 @@ -spring: - application: - name: participation-service - - datasource: - url: jdbc:mysql://${DB_HOST:localhost}:${DB_PORT:3306}/${DB_NAME:participation_db}?useSSL=false&serverTimezone=UTC&characterEncoding=UTF-8 - username: ${DB_USER:root} - password: ${DB_PASSWORD:password} - driver-class-name: com.mysql.cj.jdbc.Driver - hikari: - maximum-pool-size: ${DB_POOL_SIZE:10} - minimum-idle: ${DB_MIN_IDLE:5} - connection-timeout: ${DB_CONN_TIMEOUT:30000} - idle-timeout: ${DB_IDLE_TIMEOUT:600000} - max-lifetime: ${DB_MAX_LIFETIME:1800000} - - jpa: - hibernate: - ddl-auto: ${JPA_DDL_AUTO:validate} - show-sql: ${JPA_SHOW_SQL:false} - properties: - hibernate: - format_sql: true - use_sql_comments: true - dialect: org.hibernate.dialect.MySQL8Dialect - - # Redis Configuration - data: - redis: - host: ${REDIS_HOST:localhost} - port: ${REDIS_PORT:6379} - password: ${REDIS_PASSWORD:} - timeout: ${REDIS_TIMEOUT:3000} - lettuce: - pool: - max-active: ${REDIS_POOL_MAX_ACTIVE:8} - max-idle: ${REDIS_POOL_MAX_IDLE:8} - min-idle: ${REDIS_POOL_MIN_IDLE:2} - max-wait: ${REDIS_POOL_MAX_WAIT:3000} - - # Kafka Configuration - kafka: - bootstrap-servers: ${KAFKA_BOOTSTRAP_SERVERS:localhost:9092} - producer: - key-serializer: org.apache.kafka.common.serialization.StringSerializer - value-serializer: org.springframework.kafka.support.serializer.JsonSerializer - acks: ${KAFKA_PRODUCER_ACKS:all} - retries: ${KAFKA_PRODUCER_RETRIES:3} - properties: - max.in.flight.requests.per.connection: 1 - enable.idempotence: true - # Topic Names - topics: - participant-registered: participant-events - -server: - port: ${SERVER_PORT:8084} - servlet: - context-path: / - error: - include-message: always - include-binding-errors: always - -# Logging -logging: - level: - root: ${LOG_LEVEL_ROOT:INFO} - com.kt.event.participation: ${LOG_LEVEL_APP:DEBUG} - org.springframework.data.redis: ${LOG_LEVEL_REDIS:INFO} - org.springframework.kafka: ${LOG_LEVEL_KAFKA:INFO} - pattern: - console: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" - file: "%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n" - file: - name: ${LOG_FILE_PATH:./logs}/participation-service.log - max-size: ${LOG_FILE_MAX_SIZE:10MB} - max-history: ${LOG_FILE_MAX_HISTORY:30} - -# Application-specific Configuration -app: - cache: - duplicate-check-ttl: ${CACHE_DUPLICATE_TTL:604800} # 7 days in seconds - participant-list-ttl: ${CACHE_PARTICIPANT_TTL:600} # 10 minutes in seconds - lottery: - algorithm: FISHER_YATES_SHUFFLE - visit-bonus-weight: ${LOTTERY_VISIT_BONUS:2.0} # 매장 방문 고객 가중치 - security: - phone-mask-pattern: "***-****-***" # 전화번호 마스킹 패턴