mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 10:46:23 +00:00
Participation Service 백엔드 개발 완료
- 이벤트 참여 API 구현 - 참여자 목록/상세 조회 API 구현 - 당첨자 추첨 및 조회 API 구현 - PostgreSQL 데이터베이스 연동 - Kafka 이벤트 발행 연동 - 로깅 설정 및 실행 프로파일 추가 - .gradle 폴더 Git 추적 제거 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
c6de9bd1d0
commit
04d417e34c
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,2 +0,0 @@
|
||||
#Thu Oct 23 17:51:21 KST 2025
|
||||
gradle.version=8.10
|
||||
Binary file not shown.
Binary file not shown.
@ -64,11 +64,14 @@ public enum ErrorCode {
|
||||
DIST_004("DIST_004", "배포 상태를 찾을 수 없습니다"),
|
||||
|
||||
// 참여 에러 (PART_XXX)
|
||||
PART_001("PART_001", "이미 참여한 이벤트입니다"),
|
||||
PART_002("PART_002", "이벤트 참여 기간이 아닙니다"),
|
||||
PART_003("PART_003", "참여자를 찾을 수 없습니다"),
|
||||
PART_004("PART_004", "당첨자 추첨에 실패했습니다"),
|
||||
PART_005("PART_005", "이벤트가 종료되었습니다"),
|
||||
DUPLICATE_PARTICIPATION("PART_001", "이미 참여한 이벤트입니다"),
|
||||
EVENT_NOT_ACTIVE("PART_002", "이벤트 참여 기간이 아닙니다"),
|
||||
PARTICIPANT_NOT_FOUND("PART_003", "참여자를 찾을 수 없습니다"),
|
||||
DRAW_FAILED("PART_004", "당첨자 추첨에 실패했습니다"),
|
||||
EVENT_ENDED("PART_005", "이벤트가 종료되었습니다"),
|
||||
ALREADY_DRAWN("PART_006", "이미 당첨자 추첨이 완료되었습니다"),
|
||||
INSUFFICIENT_PARTICIPANTS("PART_007", "참여자 수가 당첨자 수보다 적습니다"),
|
||||
NO_WINNERS_YET("PART_008", "아직 당첨자 추첨이 진행되지 않았습니다"),
|
||||
|
||||
// 분석 에러 (ANALYTICS_XXX)
|
||||
ANALYTICS_001("ANALYTICS_001", "분석 데이터를 찾을 수 없습니다"),
|
||||
|
||||
@ -1,7 +1,50 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot'
|
||||
id 'io.spring.dependency-management'
|
||||
}
|
||||
|
||||
group = 'com.kt.event'
|
||||
version = '1.0.0'
|
||||
sourceCompatibility = '21'
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// Kafka for event publishing
|
||||
// Common 모듈
|
||||
implementation project(':common')
|
||||
|
||||
// Spring Boot Starters
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.kafka:spring-kafka'
|
||||
|
||||
// PostgreSQL
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
|
||||
// Lombok
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
// Jackson for JSON
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||
|
||||
// Test
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.kafka:spring-kafka-test'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
||||
@ -0,0 +1,23 @@
|
||||
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 Main Application
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {
|
||||
"com.kt.event.participation",
|
||||
"com.kt.event.common"
|
||||
})
|
||||
@EnableJpaAuditing
|
||||
public class ParticipationServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(ParticipationServiceApplication.class, args);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,21 @@
|
||||
package com.kt.event.participation.application.dto;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 당첨자 추첨 요청 DTO
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class DrawWinnersRequest {
|
||||
|
||||
@NotNull(message = "당첨자 수는 필수입니다")
|
||||
@Min(value = 1, message = "당첨자 수는 최소 1명 이상이어야 합니다")
|
||||
private Integer winnerCount;
|
||||
|
||||
@Builder.Default
|
||||
private Boolean applyStoreVisitBonus = true;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.kt.event.participation.application.dto;
|
||||
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 당첨자 추첨 응답 DTO
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class DrawWinnersResponse {
|
||||
|
||||
private String eventId;
|
||||
private Integer totalParticipants;
|
||||
private Integer winnerCount;
|
||||
private LocalDateTime drawnAt;
|
||||
private List<WinnerSummary> winners;
|
||||
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public static class WinnerSummary {
|
||||
private String participantId;
|
||||
private String name;
|
||||
private String phoneNumber;
|
||||
private Integer rank;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,34 @@
|
||||
package com.kt.event.participation.application.dto;
|
||||
|
||||
import jakarta.validation.constraints.*;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 이벤트 참여 요청 DTO
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ParticipationRequest {
|
||||
|
||||
@NotBlank(message = "이름은 필수입니다")
|
||||
@Size(min = 2, max = 50, message = "이름은 2자 이상 50자 이하여야 합니다")
|
||||
private String name;
|
||||
|
||||
@NotBlank(message = "전화번호는 필수입니다")
|
||||
@Pattern(regexp = "^\\d{3}-\\d{3,4}-\\d{4}$", message = "전화번호 형식이 올바르지 않습니다")
|
||||
private String phoneNumber;
|
||||
|
||||
@Email(message = "이메일 형식이 올바르지 않습니다")
|
||||
private String email;
|
||||
|
||||
@Builder.Default
|
||||
private Boolean agreeMarketing = false;
|
||||
|
||||
@NotNull(message = "개인정보 수집 및 이용 동의는 필수입니다")
|
||||
private Boolean agreePrivacy;
|
||||
|
||||
@Builder.Default
|
||||
private Boolean storeVisited = false;
|
||||
}
|
||||
@ -0,0 +1,40 @@
|
||||
package com.kt.event.participation.application.dto;
|
||||
|
||||
import com.kt.event.participation.domain.participant.Participant;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 이벤트 참여 응답 DTO
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ParticipationResponse {
|
||||
|
||||
private String participantId;
|
||||
private String eventId;
|
||||
private String name;
|
||||
private String phoneNumber;
|
||||
private String email;
|
||||
private LocalDateTime participatedAt;
|
||||
private Boolean storeVisited;
|
||||
private Integer bonusEntries;
|
||||
private Boolean isWinner;
|
||||
|
||||
public static ParticipationResponse from(Participant participant) {
|
||||
return ParticipationResponse.builder()
|
||||
.participantId(participant.getParticipantId())
|
||||
.eventId(participant.getEventId())
|
||||
.name(participant.getName())
|
||||
.phoneNumber(participant.getPhoneNumber())
|
||||
.email(participant.getEmail())
|
||||
.participatedAt(participant.getCreatedAt())
|
||||
.storeVisited(participant.getStoreVisited())
|
||||
.bonusEntries(participant.getBonusEntries())
|
||||
.isWinner(participant.getIsWinner())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,117 @@
|
||||
package com.kt.event.participation.application.service;
|
||||
|
||||
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.domain.participant.Participant;
|
||||
import com.kt.event.participation.domain.participant.ParticipantRepository;
|
||||
import com.kt.event.participation.exception.ParticipationException.*;
|
||||
import com.kt.event.participation.infrastructure.kafka.KafkaProducerService;
|
||||
import com.kt.event.participation.infrastructure.kafka.event.ParticipantRegisteredEvent;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
/**
|
||||
* 이벤트 참여 서비스
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class ParticipationService {
|
||||
|
||||
private final ParticipantRepository participantRepository;
|
||||
private final KafkaProducerService kafkaProducerService;
|
||||
|
||||
/**
|
||||
* 이벤트 참여
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request 참여 요청
|
||||
* @return 참여 응답
|
||||
*/
|
||||
@Transactional
|
||||
public ParticipationResponse participate(String eventId, ParticipationRequest request) {
|
||||
log.info("이벤트 참여 시작 - eventId: {}, phoneNumber: {}", eventId, request.getPhoneNumber());
|
||||
|
||||
// 중복 참여 체크
|
||||
if (participantRepository.existsByEventIdAndPhoneNumber(eventId, request.getPhoneNumber())) {
|
||||
throw new DuplicateParticipationException();
|
||||
}
|
||||
|
||||
// 참여자 ID 생성
|
||||
Long maxId = participantRepository.findMaxIdByEventId(eventId).orElse(0L);
|
||||
String participantId = Participant.generateParticipantId(eventId, maxId + 1);
|
||||
|
||||
// 참여자 저장
|
||||
Participant participant = Participant.builder()
|
||||
.participantId(participantId)
|
||||
.eventId(eventId)
|
||||
.name(request.getName())
|
||||
.phoneNumber(request.getPhoneNumber())
|
||||
.email(request.getEmail())
|
||||
.storeVisited(request.getStoreVisited())
|
||||
.bonusEntries(Participant.calculateBonusEntries(request.getStoreVisited()))
|
||||
.agreeMarketing(request.getAgreeMarketing())
|
||||
.agreePrivacy(request.getAgreePrivacy())
|
||||
.isWinner(false)
|
||||
.build();
|
||||
|
||||
participant = participantRepository.save(participant);
|
||||
log.info("참여자 저장 완료 - participantId: {}", participantId);
|
||||
|
||||
// Kafka 이벤트 발행
|
||||
kafkaProducerService.publishParticipantRegistered(
|
||||
ParticipantRegisteredEvent.from(participant)
|
||||
);
|
||||
|
||||
return ParticipationResponse.from(participant);
|
||||
}
|
||||
|
||||
/**
|
||||
* 참여자 목록 조회
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param storeVisited 매장 방문 여부 필터 (nullable)
|
||||
* @param pageable 페이징 정보
|
||||
* @return 참여자 목록
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<ParticipationResponse> getParticipants(
|
||||
String eventId, Boolean storeVisited, Pageable pageable) {
|
||||
|
||||
Page<Participant> participantPage;
|
||||
if (storeVisited != null) {
|
||||
participantPage = participantRepository
|
||||
.findByEventIdAndStoreVisitedOrderByCreatedAtDesc(eventId, storeVisited, pageable);
|
||||
} else {
|
||||
participantPage = participantRepository
|
||||
.findByEventIdOrderByCreatedAtDesc(eventId, pageable);
|
||||
}
|
||||
|
||||
Page<ParticipationResponse> responsePage = participantPage.map(ParticipationResponse::from);
|
||||
return PageResponse.of(responsePage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 참여자 상세 조회
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param participantId 참여자 ID
|
||||
* @return 참여자 정보
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public ParticipationResponse getParticipant(String eventId, String participantId) {
|
||||
Participant participant = participantRepository
|
||||
.findByEventIdAndParticipantId(eventId, participantId)
|
||||
.orElseThrow(ParticipantNotFoundException::new);
|
||||
|
||||
return ParticipationResponse.from(participant);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,158 @@
|
||||
package com.kt.event.participation.application.service;
|
||||
|
||||
import com.kt.event.common.dto.PageResponse;
|
||||
import com.kt.event.participation.application.dto.DrawWinnersRequest;
|
||||
import com.kt.event.participation.application.dto.DrawWinnersResponse;
|
||||
import com.kt.event.participation.application.dto.DrawWinnersResponse.WinnerSummary;
|
||||
import com.kt.event.participation.application.dto.ParticipationResponse;
|
||||
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 com.kt.event.participation.exception.ParticipationException.*;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
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.util.ArrayList;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 당첨자 추첨 서비스
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class WinnerDrawService {
|
||||
|
||||
private final ParticipantRepository participantRepository;
|
||||
private final DrawLogRepository drawLogRepository;
|
||||
|
||||
/**
|
||||
* 당첨자 추첨
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param request 추첨 요청
|
||||
* @return 추첨 결과
|
||||
*/
|
||||
@Transactional
|
||||
public DrawWinnersResponse drawWinners(String eventId, DrawWinnersRequest request) {
|
||||
log.info("당첨자 추첨 시작 - eventId: {}, winnerCount: {}", eventId, request.getWinnerCount());
|
||||
|
||||
// 이미 추첨이 완료되었는지 확인
|
||||
if (drawLogRepository.existsByEventId(eventId)) {
|
||||
throw new AlreadyDrawnException();
|
||||
}
|
||||
|
||||
// 참여자 목록 조회
|
||||
List<Participant> participants = participantRepository.findByEventIdAndIsWinnerFalse(eventId);
|
||||
long participantCount = participants.size();
|
||||
|
||||
// 참여자 수 검증
|
||||
if (participantCount < request.getWinnerCount()) {
|
||||
throw new InsufficientParticipantsException(participantCount, request.getWinnerCount());
|
||||
}
|
||||
|
||||
// 가중치 적용 추첨 풀 생성
|
||||
List<Participant> drawPool = createDrawPool(participants, request.getApplyStoreVisitBonus());
|
||||
|
||||
// 추첨 실행
|
||||
Collections.shuffle(drawPool);
|
||||
List<Participant> winners = drawPool.stream()
|
||||
.distinct()
|
||||
.limit(request.getWinnerCount())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
// 당첨자 업데이트
|
||||
LocalDateTime now = LocalDateTime.now();
|
||||
for (int i = 0; i < winners.size(); i++) {
|
||||
winners.get(i).markAsWinner(i + 1);
|
||||
}
|
||||
participantRepository.saveAll(winners);
|
||||
|
||||
// 추첨 로그 저장
|
||||
DrawLog drawLog = DrawLog.builder()
|
||||
.eventId(eventId)
|
||||
.totalParticipants((int) participantCount)
|
||||
.winnerCount(request.getWinnerCount())
|
||||
.applyStoreVisitBonus(request.getApplyStoreVisitBonus())
|
||||
.algorithm("WEIGHTED_RANDOM")
|
||||
.drawnAt(now)
|
||||
.drawnBy("SYSTEM")
|
||||
.build();
|
||||
drawLogRepository.save(drawLog);
|
||||
|
||||
log.info("당첨자 추첨 완료 - eventId: {}, winners: {}", eventId, winners.size());
|
||||
|
||||
// 응답 생성
|
||||
List<WinnerSummary> winnerSummaries = winners.stream()
|
||||
.map(w -> WinnerSummary.builder()
|
||||
.participantId(w.getParticipantId())
|
||||
.name(w.getName())
|
||||
.phoneNumber(w.getPhoneNumber())
|
||||
.rank(w.getWinnerRank())
|
||||
.build())
|
||||
.collect(Collectors.toList());
|
||||
|
||||
return DrawWinnersResponse.builder()
|
||||
.eventId(eventId)
|
||||
.totalParticipants((int) participantCount)
|
||||
.winnerCount(winners.size())
|
||||
.drawnAt(now)
|
||||
.winners(winnerSummaries)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 당첨자 목록 조회
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param pageable 페이징 정보
|
||||
* @return 당첨자 목록
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public PageResponse<ParticipationResponse> getWinners(String eventId, Pageable pageable) {
|
||||
// 추첨 완료 확인
|
||||
if (!drawLogRepository.existsByEventId(eventId)) {
|
||||
throw new NoWinnersYetException();
|
||||
}
|
||||
|
||||
Page<Participant> winnerPage = participantRepository
|
||||
.findByEventIdAndIsWinnerTrueOrderByWinnerRankAsc(eventId, pageable);
|
||||
|
||||
Page<ParticipationResponse> responsePage = winnerPage.map(ParticipationResponse::from);
|
||||
return PageResponse.of(responsePage);
|
||||
}
|
||||
|
||||
/**
|
||||
* 추첨 풀 생성 (매장 방문 보너스 적용)
|
||||
*
|
||||
* @param participants 참여자 목록
|
||||
* @param applyBonus 보너스 적용 여부
|
||||
* @return 추첨 풀
|
||||
*/
|
||||
private List<Participant> createDrawPool(List<Participant> participants, Boolean applyBonus) {
|
||||
if (!applyBonus) {
|
||||
return new ArrayList<>(participants);
|
||||
}
|
||||
|
||||
List<Participant> pool = new ArrayList<>();
|
||||
for (Participant participant : participants) {
|
||||
// 보너스 응모권 수만큼 추첨 풀에 추가
|
||||
int entries = participant.getBonusEntries();
|
||||
for (int i = 0; i < entries; i++) {
|
||||
pool.add(participant);
|
||||
}
|
||||
}
|
||||
return pool;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,71 @@
|
||||
package com.kt.event.participation.domain.draw;
|
||||
|
||||
import com.kt.event.common.entity.BaseTimeEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 당첨자 추첨 로그 엔티티
|
||||
* 추첨 이력 관리 및 재추첨 방지
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "draw_logs",
|
||||
indexes = {
|
||||
@Index(name = "idx_event_id", columnList = "event_id")
|
||||
}
|
||||
)
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class DrawLog extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 이벤트 ID
|
||||
*/
|
||||
@Column(name = "event_id", nullable = false, length = 50)
|
||||
private String eventId;
|
||||
|
||||
/**
|
||||
* 전체 참여자 수
|
||||
*/
|
||||
@Column(name = "total_participants", nullable = false)
|
||||
private Integer totalParticipants;
|
||||
|
||||
/**
|
||||
* 당첨자 수
|
||||
*/
|
||||
@Column(name = "winner_count", nullable = false)
|
||||
private Integer winnerCount;
|
||||
|
||||
/**
|
||||
* 매장 방문 보너스 적용 여부
|
||||
*/
|
||||
@Column(name = "apply_store_visit_bonus", nullable = false)
|
||||
private Boolean applyStoreVisitBonus;
|
||||
|
||||
/**
|
||||
* 추첨 알고리즘
|
||||
*/
|
||||
@Column(name = "algorithm", nullable = false, length = 50)
|
||||
private String algorithm;
|
||||
|
||||
/**
|
||||
* 추첨 일시
|
||||
*/
|
||||
@Column(name = "drawn_at", nullable = false)
|
||||
private java.time.LocalDateTime drawnAt;
|
||||
|
||||
/**
|
||||
* 추첨 실행자 ID (관리자 또는 시스템)
|
||||
*/
|
||||
@Column(name = "drawn_by", length = 50)
|
||||
private String drawnBy;
|
||||
}
|
||||
@ -0,0 +1,33 @@
|
||||
package com.kt.event.participation.domain.draw;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 추첨 로그 리포지토리
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Repository
|
||||
public interface DrawLogRepository extends JpaRepository<DrawLog, Long> {
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 추첨 로그 조회
|
||||
* 이미 추첨이 진행되었는지 확인
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 추첨 로그 Optional
|
||||
*/
|
||||
Optional<DrawLog> findByEventId(String eventId);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 추첨 여부 확인
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 추첨 여부
|
||||
*/
|
||||
boolean existsByEventId(String eventId);
|
||||
}
|
||||
@ -0,0 +1,162 @@
|
||||
package com.kt.event.participation.domain.participant;
|
||||
|
||||
import com.kt.event.common.entity.BaseTimeEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.*;
|
||||
|
||||
/**
|
||||
* 이벤트 참여자 엔티티
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "participants",
|
||||
indexes = {
|
||||
@Index(name = "idx_event_id", columnList = "event_id"),
|
||||
@Index(name = "idx_event_phone", columnList = "event_id, phone_number")
|
||||
},
|
||||
uniqueConstraints = {
|
||||
@UniqueConstraint(name = "uk_event_phone", columnNames = {"event_id", "phone_number"})
|
||||
}
|
||||
)
|
||||
@Getter
|
||||
@NoArgsConstructor(access = AccessLevel.PROTECTED)
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class Participant extends BaseTimeEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 참여자 ID (외부 노출용)
|
||||
* 예: prt_20250123_001
|
||||
*/
|
||||
@Column(name = "participant_id", nullable = false, unique = true, length = 50)
|
||||
private String participantId;
|
||||
|
||||
/**
|
||||
* 이벤트 ID
|
||||
* Event Service의 이벤트 식별자
|
||||
*/
|
||||
@Column(name = "event_id", nullable = false, length = 50)
|
||||
private String eventId;
|
||||
|
||||
/**
|
||||
* 참여자 이름
|
||||
*/
|
||||
@Column(name = "name", nullable = false, length = 50)
|
||||
private String name;
|
||||
|
||||
/**
|
||||
* 참여자 전화번호
|
||||
* 중복 참여 체크 키로 사용
|
||||
*/
|
||||
@Column(name = "phone_number", nullable = false, length = 20)
|
||||
private String phoneNumber;
|
||||
|
||||
/**
|
||||
* 참여자 이메일
|
||||
*/
|
||||
@Column(name = "email", length = 100)
|
||||
private String email;
|
||||
|
||||
/**
|
||||
* 매장 방문 여부
|
||||
* true일 경우 보너스 응모권 부여
|
||||
*/
|
||||
@Column(name = "store_visited", nullable = false)
|
||||
private Boolean storeVisited;
|
||||
|
||||
/**
|
||||
* 보너스 응모권 수
|
||||
* 기본 1, 매장 방문 시 +1
|
||||
*/
|
||||
@Column(name = "bonus_entries", nullable = false)
|
||||
private Integer bonusEntries;
|
||||
|
||||
/**
|
||||
* 마케팅 정보 수신 동의
|
||||
*/
|
||||
@Column(name = "agree_marketing", nullable = false)
|
||||
private Boolean agreeMarketing;
|
||||
|
||||
/**
|
||||
* 개인정보 수집 및 이용 동의 (필수)
|
||||
*/
|
||||
@Column(name = "agree_privacy", nullable = false)
|
||||
private Boolean agreePrivacy;
|
||||
|
||||
/**
|
||||
* 당첨 여부
|
||||
*/
|
||||
@Column(name = "is_winner", nullable = false)
|
||||
private Boolean isWinner;
|
||||
|
||||
/**
|
||||
* 당첨 순위 (당첨자일 경우)
|
||||
*/
|
||||
@Column(name = "winner_rank")
|
||||
private Integer winnerRank;
|
||||
|
||||
/**
|
||||
* 당첨 일시
|
||||
*/
|
||||
@Column(name = "won_at")
|
||||
private java.time.LocalDateTime wonAt;
|
||||
|
||||
/**
|
||||
* 참여자 ID 생성
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param sequenceNumber 순번
|
||||
* @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);
|
||||
}
|
||||
|
||||
/**
|
||||
* 보너스 응모권 계산
|
||||
*
|
||||
* @param storeVisited 매장 방문 여부
|
||||
* @return 보너스 응모권 수
|
||||
*/
|
||||
public static Integer calculateBonusEntries(Boolean storeVisited) {
|
||||
return storeVisited ? 2 : 1;
|
||||
}
|
||||
|
||||
/**
|
||||
* 당첨자로 설정
|
||||
*
|
||||
* @param rank 당첨 순위
|
||||
*/
|
||||
public void markAsWinner(Integer rank) {
|
||||
this.isWinner = true;
|
||||
this.winnerRank = rank;
|
||||
this.wonAt = java.time.LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 참여자 생성 전 유효성 검증
|
||||
*/
|
||||
@PrePersist
|
||||
public void prePersist() {
|
||||
if (this.agreePrivacy == null || !this.agreePrivacy) {
|
||||
throw new IllegalStateException("개인정보 수집 및 이용 동의는 필수입니다");
|
||||
}
|
||||
if (this.bonusEntries == null) {
|
||||
this.bonusEntries = calculateBonusEntries(this.storeVisited);
|
||||
}
|
||||
if (this.isWinner == null) {
|
||||
this.isWinner = false;
|
||||
}
|
||||
if (this.agreeMarketing == null) {
|
||||
this.agreeMarketing = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,109 @@
|
||||
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;
|
||||
|
||||
/**
|
||||
* 참여자 리포지토리
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Repository
|
||||
public interface ParticipantRepository extends JpaRepository<Participant, Long> {
|
||||
|
||||
/**
|
||||
* 참여자 ID로 조회
|
||||
*
|
||||
* @param participantId 참여자 ID
|
||||
* @return 참여자 Optional
|
||||
*/
|
||||
Optional<Participant> findByParticipantId(String participantId);
|
||||
|
||||
/**
|
||||
* 이벤트 ID와 전화번호로 중복 참여 체크
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param phoneNumber 전화번호
|
||||
* @return 참여 여부
|
||||
*/
|
||||
boolean existsByEventIdAndPhoneNumber(String eventId, String phoneNumber);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 참여자 목록 조회 (페이징)
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param pageable 페이징 정보
|
||||
* @return 참여자 페이지
|
||||
*/
|
||||
Page<Participant> findByEventIdOrderByCreatedAtDesc(String eventId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 이벤트 ID와 매장 방문 여부로 참여자 목록 조회 (페이징)
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param storeVisited 매장 방문 여부
|
||||
* @param pageable 페이징 정보
|
||||
* @return 참여자 페이지
|
||||
*/
|
||||
Page<Participant> findByEventIdAndStoreVisitedOrderByCreatedAtDesc(
|
||||
String eventId, Boolean storeVisited, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 전체 참여자 수 조회
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 참여자 수
|
||||
*/
|
||||
long countByEventId(String eventId);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 당첨자 목록 조회 (페이징)
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param pageable 페이징 정보
|
||||
* @return 당첨자 페이지
|
||||
*/
|
||||
Page<Participant> findByEventIdAndIsWinnerTrueOrderByWinnerRankAsc(String eventId, Pageable pageable);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 당첨자 수 조회
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 당첨자 수
|
||||
*/
|
||||
long countByEventIdAndIsWinnerTrue(String eventId);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 참여자 ID 최대값 조회 (순번 생성용)
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 최대 ID
|
||||
*/
|
||||
@Query("SELECT MAX(p.id) FROM Participant p WHERE p.eventId = :eventId")
|
||||
Optional<Long> findMaxIdByEventId(@Param("eventId") String eventId);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 비당첨자 목록 조회 (추첨용)
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 비당첨자 목록
|
||||
*/
|
||||
List<Participant> findByEventIdAndIsWinnerFalse(String eventId);
|
||||
|
||||
/**
|
||||
* 이벤트 ID와 참여자 ID로 조회
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param participantId 참여자 ID
|
||||
* @return 참여자 Optional
|
||||
*/
|
||||
Optional<Participant> findByEventIdAndParticipantId(String eventId, String participantId);
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package com.kt.event.participation.exception;
|
||||
|
||||
import com.kt.event.common.exception.BusinessException;
|
||||
import com.kt.event.common.exception.ErrorCode;
|
||||
|
||||
/**
|
||||
* 참여 관련 비즈니스 예외
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
public class ParticipationException extends BusinessException {
|
||||
|
||||
public ParticipationException(ErrorCode errorCode) {
|
||||
super(errorCode);
|
||||
}
|
||||
|
||||
public ParticipationException(ErrorCode errorCode, String message) {
|
||||
super(errorCode, message);
|
||||
}
|
||||
|
||||
/**
|
||||
* 중복 참여 예외
|
||||
*/
|
||||
public static class DuplicateParticipationException extends ParticipationException {
|
||||
public DuplicateParticipationException() {
|
||||
super(ErrorCode.DUPLICATE_PARTICIPATION, "이미 참여하신 이벤트입니다");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트를 찾을 수 없음 예외
|
||||
*/
|
||||
public static class EventNotFoundException extends ParticipationException {
|
||||
public EventNotFoundException() {
|
||||
super(ErrorCode.EVENT_001, "이벤트를 찾을 수 없습니다");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이벤트가 활성 상태가 아님 예외
|
||||
*/
|
||||
public static class EventNotActiveException extends ParticipationException {
|
||||
public EventNotActiveException() {
|
||||
super(ErrorCode.EVENT_NOT_ACTIVE, "현재 참여할 수 없는 이벤트입니다");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 참여자를 찾을 수 없음 예외
|
||||
*/
|
||||
public static class ParticipantNotFoundException extends ParticipationException {
|
||||
public ParticipantNotFoundException() {
|
||||
super(ErrorCode.PARTICIPANT_NOT_FOUND, "참여자를 찾을 수 없습니다");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 이미 추첨이 완료됨 예외
|
||||
*/
|
||||
public static class AlreadyDrawnException extends ParticipationException {
|
||||
public AlreadyDrawnException() {
|
||||
super(ErrorCode.ALREADY_DRAWN, "이미 당첨자 추첨이 완료되었습니다");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 참여자 수 부족 예외
|
||||
*/
|
||||
public static class InsufficientParticipantsException extends ParticipationException {
|
||||
public InsufficientParticipantsException(long participantCount, int winnerCount) {
|
||||
super(ErrorCode.INSUFFICIENT_PARTICIPANTS,
|
||||
String.format("참여자 수(%d)가 당첨자 수(%d)보다 적습니다", participantCount, winnerCount));
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 당첨자가 없음 예외
|
||||
*/
|
||||
public static class NoWinnersYetException extends ParticipationException {
|
||||
public NoWinnersYetException() {
|
||||
super(ErrorCode.NO_WINNERS_YET, "아직 당첨자 추첨이 진행되지 않았습니다");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,32 @@
|
||||
package com.kt.event.participation.infrastructure.config;
|
||||
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||
import org.springframework.security.config.http.SessionCreationPolicy;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
|
||||
/**
|
||||
* Security Configuration for Participation Service
|
||||
* 이벤트 참여 API는 공개 API로 인증 불필요
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Configuration
|
||||
@EnableWebSecurity
|
||||
public class SecurityConfig {
|
||||
|
||||
@Bean
|
||||
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
|
||||
http
|
||||
.csrf(csrf -> csrf.disable())
|
||||
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
|
||||
.authorizeHttpRequests(auth -> auth
|
||||
.anyRequest().permitAll()
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
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.kafka.core.KafkaTemplate;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
/**
|
||||
* Kafka Producer 서비스
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class KafkaProducerService {
|
||||
|
||||
private static final String PARTICIPANT_REGISTERED_TOPIC = "participant-registered-events";
|
||||
|
||||
private final KafkaTemplate<String, Object> kafkaTemplate;
|
||||
|
||||
/**
|
||||
* 참여자 등록 이벤트 발행
|
||||
*
|
||||
* @param event 참여자 등록 이벤트
|
||||
*/
|
||||
public void publishParticipantRegistered(ParticipantRegisteredEvent event) {
|
||||
try {
|
||||
kafkaTemplate.send(PARTICIPANT_REGISTERED_TOPIC, event.getEventId(), event);
|
||||
log.info("Kafka 이벤트 발행 성공 - topic: {}, participantId: {}",
|
||||
PARTICIPANT_REGISTERED_TOPIC, event.getParticipantId());
|
||||
} catch (Exception e) {
|
||||
log.error("Kafka 이벤트 발행 실패 - participantId: {}", event.getParticipantId(), e);
|
||||
// 이벤트 발행 실패는 서비스 로직에 영향을 주지 않음
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,39 @@
|
||||
package com.kt.event.participation.infrastructure.kafka.event;
|
||||
|
||||
import com.kt.event.participation.domain.participant.Participant;
|
||||
import lombok.*;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 참여자 등록 Kafka 이벤트
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Getter
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Builder
|
||||
public class ParticipantRegisteredEvent {
|
||||
|
||||
private String participantId;
|
||||
private String eventId;
|
||||
private String name;
|
||||
private String phoneNumber;
|
||||
private Boolean storeVisited;
|
||||
private Integer bonusEntries;
|
||||
private LocalDateTime participatedAt;
|
||||
|
||||
public static ParticipantRegisteredEvent from(Participant participant) {
|
||||
return ParticipantRegisteredEvent.builder()
|
||||
.participantId(participant.getParticipantId())
|
||||
.eventId(participant.getEventId())
|
||||
.name(participant.getName())
|
||||
.phoneNumber(participant.getPhoneNumber())
|
||||
.storeVisited(participant.getStoreVisited())
|
||||
.bonusEntries(participant.getBonusEntries())
|
||||
.participatedAt(participant.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,79 @@
|
||||
package com.kt.event.participation.presentation.controller;
|
||||
|
||||
import com.kt.event.common.dto.ApiResponse;
|
||||
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 jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 이벤트 참여 컨트롤러
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping
|
||||
@RequiredArgsConstructor
|
||||
public class ParticipationController {
|
||||
|
||||
private final ParticipationService participationService;
|
||||
|
||||
/**
|
||||
* 이벤트 참여
|
||||
* POST /events/{eventId}/participate
|
||||
*/
|
||||
@PostMapping("/events/{eventId}/participate")
|
||||
public ResponseEntity<ApiResponse<ParticipationResponse>> participate(
|
||||
@PathVariable String eventId,
|
||||
@Valid @RequestBody ParticipationRequest request) {
|
||||
|
||||
log.info("이벤트 참여 요청 - eventId: {}", eventId);
|
||||
ParticipationResponse response = participationService.participate(eventId, request);
|
||||
|
||||
return ResponseEntity
|
||||
.status(HttpStatus.CREATED)
|
||||
.body(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 참여자 목록 조회
|
||||
* GET /events/{eventId}/participants
|
||||
*/
|
||||
@GetMapping("/events/{eventId}/participants")
|
||||
public ResponseEntity<ApiResponse<PageResponse<ParticipationResponse>>> getParticipants(
|
||||
@PathVariable String eventId,
|
||||
@RequestParam(required = false) Boolean storeVisited,
|
||||
@PageableDefault(size = 20) Pageable pageable) {
|
||||
|
||||
log.info("참여자 목록 조회 요청 - eventId: {}, storeVisited: {}", eventId, storeVisited);
|
||||
PageResponse<ParticipationResponse> response =
|
||||
participationService.getParticipants(eventId, storeVisited, pageable);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 참여자 상세 조회
|
||||
* GET /events/{eventId}/participants/{participantId}
|
||||
*/
|
||||
@GetMapping("/events/{eventId}/participants/{participantId}")
|
||||
public ResponseEntity<ApiResponse<ParticipationResponse>> getParticipant(
|
||||
@PathVariable String eventId,
|
||||
@PathVariable String participantId) {
|
||||
|
||||
log.info("참여자 상세 조회 요청 - eventId: {}, participantId: {}", eventId, participantId);
|
||||
ParticipationResponse response = participationService.getParticipant(eventId, participantId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,60 @@
|
||||
package com.kt.event.participation.presentation.controller;
|
||||
|
||||
import com.kt.event.common.dto.ApiResponse;
|
||||
import com.kt.event.common.dto.PageResponse;
|
||||
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 jakarta.validation.Valid;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.web.PageableDefault;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
/**
|
||||
* 당첨자 추첨 컨트롤러
|
||||
*
|
||||
* @author Digital Garage Team
|
||||
* @since 2025-01-24
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping
|
||||
@RequiredArgsConstructor
|
||||
public class WinnerController {
|
||||
|
||||
private final WinnerDrawService winnerDrawService;
|
||||
|
||||
/**
|
||||
* 당첨자 추첨
|
||||
* POST /events/{eventId}/draw-winners
|
||||
*/
|
||||
@PostMapping("/events/{eventId}/draw-winners")
|
||||
public ResponseEntity<ApiResponse<DrawWinnersResponse>> drawWinners(
|
||||
@PathVariable String eventId,
|
||||
@Valid @RequestBody DrawWinnersRequest request) {
|
||||
|
||||
log.info("당첨자 추첨 요청 - eventId: {}, winnerCount: {}", eventId, request.getWinnerCount());
|
||||
DrawWinnersResponse response = winnerDrawService.drawWinners(eventId, request);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
|
||||
/**
|
||||
* 당첨자 목록 조회
|
||||
* GET /events/{eventId}/winners
|
||||
*/
|
||||
@GetMapping("/events/{eventId}/winners")
|
||||
public ResponseEntity<ApiResponse<PageResponse<ParticipationResponse>>> getWinners(
|
||||
@PathVariable String eventId,
|
||||
@PageableDefault(size = 20) Pageable pageable) {
|
||||
|
||||
log.info("당첨자 목록 조회 요청 - eventId: {}", eventId);
|
||||
PageResponse<ParticipationResponse> response = winnerDrawService.getWinners(eventId, pageable);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(response));
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user