@startuml
!theme mono

title Participation Service 클래스 다이어그램 (상세)

package "com.kt.event.participation" {

    package "presentation.controller" {
        class ParticipationController {
            - participationService: ParticipationService
            + participate(eventId: String, request: ParticipationRequest): ResponseEntity<ApiResponse<ParticipationResponse>>
            + getParticipants(eventId: String, storeVisited: Boolean, pageable: Pageable): ResponseEntity<ApiResponse<PageResponse<ParticipationResponse>>>
            + getParticipant(eventId: String, participantId: String): ResponseEntity<ApiResponse<ParticipationResponse>>
        }

        class WinnerController {
            - winnerDrawService: WinnerDrawService
            + drawWinners(eventId: String, request: DrawWinnersRequest): ResponseEntity<ApiResponse<DrawWinnersResponse>>
            + getWinners(eventId: String, pageable: Pageable): ResponseEntity<ApiResponse<PageResponse<ParticipationResponse>>>
        }

        class DebugController {
            + health(): ResponseEntity<Map<String, Object>>
        }
    }

    package "application.service" {
        class ParticipationService {
            - participantRepository: ParticipantRepository
            - kafkaProducerService: KafkaProducerService
            + participate(eventId: String, request: ParticipationRequest): ParticipationResponse
            + getParticipants(eventId: String, storeVisited: Boolean, pageable: Pageable): PageResponse<ParticipationResponse>
            + getParticipant(eventId: String, participantId: String): ParticipationResponse
        }

        class WinnerDrawService {
            - participantRepository: ParticipantRepository
            - drawLogRepository: DrawLogRepository
            + drawWinners(eventId: String, request: DrawWinnersRequest): DrawWinnersResponse
            + getWinners(eventId: String, pageable: Pageable): PageResponse<ParticipationResponse>
            - createDrawPool(participants: List<Participant>, applyBonus: Boolean): List<Participant>
        }
    }

    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<WinnerSummary>

            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<Participant>
            + findByEventIdOrderByCreatedAtDesc(eventId: String, pageable: Pageable): Page<Participant>
            + findByEventIdAndStoreVisitedOrderByCreatedAtDesc(eventId: String, storeVisited: Boolean, pageable: Pageable): Page<Participant>
            + findByEventIdAndParticipantId(eventId: String, participantId: String): Optional<Participant>
            + countByEventId(eventId: String): long
            + findByEventIdAndIsWinnerTrueOrderByWinnerRankAsc(eventId: String, pageable: Pageable): Page<Participant>
        }
    }

    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<String, Object>
            + 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<T>" {
            - success: boolean
            - data: T
            - errorCode: String
            - message: String
            - timestamp: LocalDateTime
            + success(data: T): ApiResponse<T>
            + error(errorCode: String, message: String): ApiResponse<T>
        }

        class "PageResponse<T>" {
            - content: List<T>
            - totalElements: long
            - totalPages: int
            - number: int
            - size: int
            - first: boolean
            - last: boolean
            + of(page: Page<T>): PageResponse<T>
        }
    }

    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<T>" : uses
ParticipationController --> "PageResponse<T>" : uses

WinnerController --> WinnerDrawService : uses
WinnerController --> DrawWinnersRequest : uses
WinnerController --> DrawWinnersResponse : uses
WinnerController --> ParticipationResponse : uses
WinnerController --> "ApiResponse<T>" : uses
WinnerController --> "PageResponse<T>" : 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<T>" : 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<T>" : 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
