mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 20:46:24 +00:00
✨ 주요 기능 - Azure 기반 물리아키텍처 설계 (개발환경/운영환경) - 7개 마이크로서비스 물리 구조 설계 - 네트워크 아키텍처 다이어그램 작성 (Mermaid) - 환경별 비교 분석 및 마스터 인덱스 문서 📁 생성 파일 - design/backend/physical/physical-architecture.md (마스터) - design/backend/physical/physical-architecture-dev.md (개발환경) - design/backend/physical/physical-architecture-prod.md (운영환경) - design/backend/physical/*.mmd (4개 Mermaid 다이어그램) 🎯 핵심 성과 - 비용 최적화: 개발환경 월 $143, 운영환경 월 $2,860 - 확장성: 개발환경 100명 → 운영환경 10,000명 (100배) - 가용성: 개발환경 95% → 운영환경 99.9% - 보안: 다층 보안 아키텍처 (L1~L4) 🛠️ 기술 스택 - Azure Kubernetes Service (AKS) - Azure Database for PostgreSQL Flexible - Azure Cache for Redis Premium - Azure Service Bus Premium - Application Gateway + WAF 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
329 lines
12 KiB
Plaintext
329 lines
12 KiB
Plaintext
@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
|