@startuml !theme mono title Participation Service 클래스 다이어그램 (상세) package "com.kt.event.participation" { package "presentation.controller" { class ParticipationController { - participationService: ParticipationService + participate(eventId: String, request: ParticipationRequest): ResponseEntity> + getParticipants(eventId: String, storeVisited: Boolean, pageable: Pageable): ResponseEntity>> + getParticipant(eventId: String, participantId: String): ResponseEntity> } class WinnerController { - winnerDrawService: WinnerDrawService + drawWinners(eventId: String, request: DrawWinnersRequest): ResponseEntity> + getWinners(eventId: String, pageable: Pageable): ResponseEntity>> } class DebugController { + health(): ResponseEntity> } } package "application.service" { class ParticipationService { - participantRepository: ParticipantRepository - kafkaProducerService: KafkaProducerService + participate(eventId: String, request: ParticipationRequest): ParticipationResponse + getParticipants(eventId: String, storeVisited: Boolean, pageable: Pageable): PageResponse + getParticipant(eventId: String, participantId: String): ParticipationResponse } class WinnerDrawService { - participantRepository: ParticipantRepository - drawLogRepository: DrawLogRepository + drawWinners(eventId: String, request: DrawWinnersRequest): DrawWinnersResponse + getWinners(eventId: String, pageable: Pageable): PageResponse - createDrawPool(participants: List, applyBonus: Boolean): List } } package "application.dto" { class ParticipationRequest { - name: String - phoneNumber: String - email: String - channel: String - storeVisited: Boolean - agreeMarketing: Boolean - agreePrivacy: Boolean } class ParticipationResponse { - participantId: String - eventId: String - name: String - phoneNumber: String - email: String - channel: String - storeVisited: Boolean - bonusEntries: Integer - agreeMarketing: Boolean - agreePrivacy: Boolean - isWinner: Boolean - winnerRank: Integer - wonAt: LocalDateTime - participatedAt: LocalDateTime + from(participant: Participant): ParticipationResponse } class DrawWinnersRequest { - winnerCount: Integer - applyStoreVisitBonus: Boolean } class DrawWinnersResponse { - eventId: String - totalParticipants: Integer - winnerCount: Integer - drawnAt: LocalDateTime - winners: List class WinnerSummary { - participantId: String - name: String - phoneNumber: String - rank: Integer } } } package "domain.participant" { class Participant extends BaseTimeEntity { - id: Long - participantId: String - eventId: String - name: String - phoneNumber: String - email: String - channel: String - storeVisited: Boolean - bonusEntries: Integer - agreeMarketing: Boolean - agreePrivacy: Boolean - isWinner: Boolean - winnerRank: Integer - wonAt: LocalDateTime + generateParticipantId(eventId: String, sequenceNumber: Long): String + calculateBonusEntries(storeVisited: Boolean): Integer + markAsWinner(rank: Integer): void + prePersist(): void } interface ParticipantRepository extends JpaRepository { + existsByEventIdAndPhoneNumber(eventId: String, phoneNumber: String): boolean + findMaxSequenceByDatePrefix(datePrefix: String): Integer + findByEventIdAndIsWinnerFalse(eventId: String): List + findByEventIdOrderByCreatedAtDesc(eventId: String, pageable: Pageable): Page + findByEventIdAndStoreVisitedOrderByCreatedAtDesc(eventId: String, storeVisited: Boolean, pageable: Pageable): Page + findByEventIdAndParticipantId(eventId: String, participantId: String): Optional + countByEventId(eventId: String): long + findByEventIdAndIsWinnerTrueOrderByWinnerRankAsc(eventId: String, pageable: Pageable): Page } } package "domain.draw" { class DrawLog extends BaseTimeEntity { - id: Long - eventId: String - totalParticipants: Integer - winnerCount: Integer - applyStoreVisitBonus: Boolean - algorithm: String - drawnAt: LocalDateTime - drawnBy: String } interface DrawLogRepository extends JpaRepository { + existsByEventId(eventId: String): boolean } } package "exception" { class ParticipationException extends BusinessException { + ParticipationException(errorCode: ErrorCode) + ParticipationException(errorCode: ErrorCode, message: String) } class DuplicateParticipationException extends ParticipationException { + DuplicateParticipationException() } class EventNotFoundException extends ParticipationException { + EventNotFoundException() } class EventNotActiveException extends ParticipationException { + EventNotActiveException() } class ParticipantNotFoundException extends ParticipationException { + ParticipantNotFoundException() } class AlreadyDrawnException extends ParticipationException { + AlreadyDrawnException() } class InsufficientParticipantsException extends ParticipationException { + InsufficientParticipantsException(participantCount: long, winnerCount: int) } class NoWinnersYetException extends ParticipationException { + NoWinnersYetException() } } package "infrastructure.kafka" { class KafkaProducerService { - PARTICIPANT_REGISTERED_TOPIC: String - kafkaTemplate: KafkaTemplate + publishParticipantRegistered(event: ParticipantRegisteredEvent): void } package "event" { class ParticipantRegisteredEvent { - eventId: String - participantId: String - name: String - phoneNumber: String - email: String - storeVisited: Boolean - bonusEntries: Integer - participatedAt: LocalDateTime + from(participant: Participant): ParticipantRegisteredEvent } } } package "infrastructure.config" { class SecurityConfig { + securityFilterChain(http: HttpSecurity): SecurityFilterChain } } } package "com.kt.event.common" { package "entity" { abstract class BaseTimeEntity { - createdAt: LocalDateTime - updatedAt: LocalDateTime } } package "dto" { class "ApiResponse" { - success: boolean - data: T - errorCode: String - message: String - timestamp: LocalDateTime + success(data: T): ApiResponse + error(errorCode: String, message: String): ApiResponse } class "PageResponse" { - content: List - totalElements: long - totalPages: int - number: int - size: int - first: boolean - last: boolean + of(page: Page): PageResponse } } package "exception" { interface ErrorCode { + getCode(): String + getMessage(): String } class BusinessException extends RuntimeException { - errorCode: ErrorCode - details: String + getErrorCode(): ErrorCode + getDetails(): String } } } ' Presentation Layer 관계 ParticipationController --> ParticipationService : uses ParticipationController --> ParticipationRequest : uses ParticipationController --> ParticipationResponse : uses ParticipationController --> "ApiResponse" : uses ParticipationController --> "PageResponse" : uses WinnerController --> WinnerDrawService : uses WinnerController --> DrawWinnersRequest : uses WinnerController --> DrawWinnersResponse : uses WinnerController --> ParticipationResponse : uses WinnerController --> "ApiResponse" : uses WinnerController --> "PageResponse" : uses ' Application Layer 관계 ParticipationService --> ParticipantRepository : uses ParticipationService --> KafkaProducerService : uses ParticipationService --> ParticipationRequest : uses ParticipationService --> ParticipationResponse : uses ParticipationService --> Participant : uses ParticipationService --> DuplicateParticipationException : throws ParticipationService --> EventNotFoundException : throws ParticipationService --> ParticipantNotFoundException : throws ParticipationService --> "PageResponse" : uses WinnerDrawService --> ParticipantRepository : uses WinnerDrawService --> DrawLogRepository : uses WinnerDrawService --> DrawWinnersRequest : uses WinnerDrawService --> DrawWinnersResponse : uses WinnerDrawService --> ParticipationResponse : uses WinnerDrawService --> Participant : uses WinnerDrawService --> DrawLog : uses WinnerDrawService --> AlreadyDrawnException : throws WinnerDrawService --> InsufficientParticipantsException : throws WinnerDrawService --> NoWinnersYetException : throws WinnerDrawService --> "PageResponse" : uses ' DTO 관계 ParticipationResponse --> Participant : converts from DrawWinnersResponse +-- DrawWinnersResponse.WinnerSummary ' Domain Layer 관계 Participant --|> BaseTimeEntity : extends DrawLog --|> BaseTimeEntity : extends ParticipantRepository --> Participant : manages DrawLogRepository --> DrawLog : manages ' Exception 관계 ParticipationException --|> BusinessException : extends ParticipationException --> ErrorCode : uses DuplicateParticipationException --|> ParticipationException : extends EventNotFoundException --|> ParticipationException : extends EventNotActiveException --|> ParticipationException : extends ParticipantNotFoundException --|> ParticipationException : extends AlreadyDrawnException --|> ParticipationException : extends InsufficientParticipantsException --|> ParticipationException : extends NoWinnersYetException --|> ParticipationException : extends ' Infrastructure Layer 관계 KafkaProducerService --> ParticipantRegisteredEvent : uses ParticipantRegisteredEvent --> Participant : converts from note top of ParticipationController : 이벤트 참여 및 참여자 조회 API\n- POST /events/{eventId}/participate\n- GET /events/{eventId}/participants\n- GET /events/{eventId}/participants/{participantId} note top of WinnerController : 당첨자 추첨 및 조회 API\n- POST /events/{eventId}/draw-winners\n- GET /events/{eventId}/winners note top of Participant : 이벤트 참여자 엔티티\n- 중복 참여 방지 (eventId + phoneNumber)\n- 매장 방문 보너스 응모권 관리\n- 당첨자 상태 관리 note top of DrawLog : 당첨자 추첨 로그\n- 추첨 이력 관리\n- 재추첨 방지 note top of KafkaProducerService : Kafka 이벤트 발행\n- 참여자 등록 이벤트 발행 @enduml