@startuml !theme mono title Analytics Service 클래스 다이어그램 (상세) ' ============================================================ ' Presentation Layer - Controller ' ============================================================ package "com.kt.event.analytics.controller" <> #F0F8FF { class AnalyticsDashboardController { - analyticsService: AnalyticsService + getEventAnalytics(eventId: String, startDate: LocalDateTime, endDate: LocalDateTime, refresh: Boolean): ResponseEntity> } class ChannelAnalyticsController { - channelAnalyticsService: ChannelAnalyticsService + getChannelAnalytics(eventId: String, channels: String, sortBy: String, sortOrder: String): ResponseEntity> } class RoiAnalyticsController { - roiAnalyticsService: RoiAnalyticsService + getRoiAnalytics(eventId: String): ResponseEntity> } class TimelineAnalyticsController { - timelineAnalyticsService: TimelineAnalyticsService + getTimelineAnalytics(eventId: String, granularity: String, startDate: LocalDateTime, endDate: LocalDateTime): ResponseEntity> } class UserAnalyticsDashboardController { - userAnalyticsService: UserAnalyticsService + getUserEventAnalytics(userId: String, startDate: LocalDateTime, endDate: LocalDateTime): ResponseEntity> } class UserChannelAnalyticsController { - userChannelAnalyticsService: UserChannelAnalyticsService + getUserChannelAnalytics(userId: String, channels: String): ResponseEntity> } class UserRoiAnalyticsController { - userRoiAnalyticsService: UserRoiAnalyticsService + getUserRoiAnalytics(userId: String): ResponseEntity> } class UserTimelineAnalyticsController { - userTimelineAnalyticsService: UserTimelineAnalyticsService + getUserTimelineAnalytics(userId: String, granularity: String, startDate: LocalDateTime, endDate: LocalDateTime): ResponseEntity> } } ' ============================================================ ' Business Layer - Service ' ============================================================ package "com.kt.event.analytics.service" <> #E6F7E6 { class AnalyticsService { - eventStatsRepository: EventStatsRepository - channelStatsRepository: ChannelStatsRepository - externalChannelService: ExternalChannelService - roiCalculator: ROICalculator - redisTemplate: RedisTemplate - objectMapper: ObjectMapper + getDashboardData(eventId: String, startDate: LocalDateTime, endDate: LocalDateTime, refresh: boolean): AnalyticsDashboardResponse - buildDashboardData(eventStats: EventStats, channelStatsList: List, startDate: LocalDateTime, endDate: LocalDateTime): AnalyticsDashboardResponse - buildPeriodInfo(startDate: LocalDateTime, endDate: LocalDateTime): PeriodInfo - buildAnalyticsSummary(eventStats: EventStats, channelStatsList: List): AnalyticsSummary - buildChannelPerformance(channelStatsList: List, totalInvestment: BigDecimal): List } class ChannelAnalyticsService { - channelStatsRepository: ChannelStatsRepository - externalChannelService: ExternalChannelService - redisTemplate: RedisTemplate - objectMapper: ObjectMapper + getChannelAnalytics(eventId: String, channels: List, sortBy: String, sortOrder: String): ChannelAnalyticsResponse - buildChannelMetrics(channelStats: ChannelStats): ChannelMetrics - buildChannelPerformance(channelStats: ChannelStats): ChannelPerformance - buildChannelComparison(channelStatsList: List): ChannelComparison } class RoiAnalyticsService { - eventStatsRepository: EventStatsRepository - channelStatsRepository: ChannelStatsRepository - roiCalculator: ROICalculator - redisTemplate: RedisTemplate - objectMapper: ObjectMapper + getRoiAnalytics(eventId: String): RoiAnalyticsResponse - buildRoiCalculation(eventStats: EventStats, channelStatsList: List): RoiCalculation - buildInvestmentDetails(channelStatsList: List): InvestmentDetails - buildRevenueDetails(eventStats: EventStats): RevenueDetails } class TimelineAnalyticsService { - timelineDataRepository: TimelineDataRepository - eventStatsRepository: EventStatsRepository - redisTemplate: RedisTemplate - objectMapper: ObjectMapper + getTimelineAnalytics(eventId: String, granularity: String, startDate: LocalDateTime, endDate: LocalDateTime): TimelineAnalyticsResponse - buildTimelineDataPoints(timelineDataList: List): List - buildTrendAnalysis(timelineDataList: List): TrendAnalysis - buildPeakTimeInfo(timelineDataList: List): PeakTimeInfo } class UserAnalyticsService { - eventStatsRepository: EventStatsRepository - channelStatsRepository: ChannelStatsRepository - roiCalculator: ROICalculator + getUserEventAnalytics(userId: String, startDate: LocalDateTime, endDate: LocalDateTime): UserAnalyticsDashboardResponse - buildUserAnalyticsSummary(eventStatsList: List, channelStatsList: List): AnalyticsSummary } class UserChannelAnalyticsService { - channelStatsRepository: ChannelStatsRepository - eventStatsRepository: EventStatsRepository + getUserChannelAnalytics(userId: String, channels: List): UserChannelAnalyticsResponse } class UserRoiAnalyticsService { - eventStatsRepository: EventStatsRepository - channelStatsRepository: ChannelStatsRepository - roiCalculator: ROICalculator + getUserRoiAnalytics(userId: String): UserRoiAnalyticsResponse } class UserTimelineAnalyticsService { - timelineDataRepository: TimelineDataRepository - eventStatsRepository: EventStatsRepository + getUserTimelineAnalytics(userId: String, granularity: String, startDate: LocalDateTime, endDate: LocalDateTime): UserTimelineAnalyticsResponse } class ExternalChannelService { + updateChannelStatsFromExternalAPIs(eventId: String, channelStatsList: List): void - updateChannelStatsFromAPI(eventId: String, channelStats: ChannelStats): void - updateWooriTVStats(eventId: String, channelStats: ChannelStats): void - wooriTVFallback(eventId: String, channelStats: ChannelStats, e: Exception): void - updateGenieTVStats(eventId: String, channelStats: ChannelStats): void - genieTVFallback(eventId: String, channelStats: ChannelStats, e: Exception): void - updateRingoBizStats(eventId: String, channelStats: ChannelStats): void - ringoBizFallback(eventId: String, channelStats: ChannelStats, e: Exception): void - updateSNSStats(eventId: String, channelStats: ChannelStats): void - snsFallback(eventId: String, channelStats: ChannelStats, e: Exception): void } class ROICalculator { + calculateRoiSummary(eventStats: EventStats): RoiSummary + calculateRoi(investment: BigDecimal, revenue: BigDecimal): BigDecimal + calculateCostPerParticipant(totalInvestment: BigDecimal, participants: int): BigDecimal + calculateRevenueProjection(currentRevenue: BigDecimal, targetRoi: BigDecimal): RevenueProjection } } ' ============================================================ ' Data Access Layer - Repository ' ============================================================ package "com.kt.event.analytics.repository" <> #FFF8DC { interface EventStatsRepository { + findByEventId(eventId: String): Optional + findByUserId(userId: String): List + save(eventStats: EventStats): EventStats + findAll(): List } interface ChannelStatsRepository { + findByEventId(eventId: String): List + findByEventIdAndChannelName(eventId: String, channelName: String): Optional + findByEventIdIn(eventIds: List): List + save(channelStats: ChannelStats): ChannelStats } interface TimelineDataRepository { + findByEventIdOrderByTimestampAsc(eventId: String): List + findByEventIdAndTimestampBetween(eventId: String, startDate: LocalDateTime, endDate: LocalDateTime): List + findByEventIdIn(eventIds: List): List + save(timelineData: TimelineData): TimelineData } } ' ============================================================ ' Domain Layer - Entity ' ============================================================ package "com.kt.event.analytics.entity" <> #FFFACD { class EventStats { - id: Long - eventId: String - eventTitle: String - userId: String - totalParticipants: Integer - totalViews: Integer - estimatedRoi: BigDecimal - targetRoi: BigDecimal - salesGrowthRate: BigDecimal - totalInvestment: BigDecimal - expectedRevenue: BigDecimal - status: String + incrementParticipants(): void + incrementParticipants(count: int): void } class ChannelStats { - id: Long - eventId: String - channelName: String - channelType: String - impressions: Integer - views: Integer - clicks: Integer - participants: Integer - conversions: Integer - distributionCost: BigDecimal - likes: Integer - comments: Integer - shares: Integer - totalCalls: Integer - completedCalls: Integer - averageDuration: Integer } class TimelineData { - id: Long - eventId: String - timestamp: LocalDateTime - participants: Integer - views: Integer - engagement: Integer - conversions: Integer - cumulativeParticipants: Integer } } ' ============================================================ ' DTO Layer ' ============================================================ package "com.kt.event.analytics.dto.response" <> #E6E6FA { class AnalyticsDashboardResponse { - eventId: String - eventTitle: String - period: PeriodInfo - summary: AnalyticsSummary - channelPerformance: List - roi: RoiSummary - lastUpdatedAt: LocalDateTime - dataSource: String } class AnalyticsSummary { - participants: Integer - participantsDelta: Integer - totalViews: Integer - totalReach: Integer - engagementRate: Double - conversionRate: Double - averageEngagementTime: Integer - targetRoi: Double - socialInteractions: SocialInteractionStats } class ChannelSummary { - channel: String - views: Integer - participants: Integer - engagementRate: Double - conversionRate: Double - roi: Double } class PeriodInfo { - startDate: LocalDateTime - endDate: LocalDateTime - durationDays: Integer } class SocialInteractionStats { - likes: Integer - comments: Integer - shares: Integer } class RoiSummary { - currentRoi: Double - targetRoi: Double - achievementRate: Double - expectedReturn: BigDecimal - totalInvestment: BigDecimal } class ChannelAnalyticsResponse { - eventId: String - eventTitle: String - totalChannels: Integer - channels: List - comparison: ChannelComparison - lastUpdatedAt: LocalDateTime } class ChannelAnalytics { - channelName: String - channelType: String - metrics: ChannelMetrics - performance: ChannelPerformance - costs: ChannelCosts - voiceCallStats: VoiceCallStats - socialStats: SocialInteractionStats } class ChannelMetrics { - impressions: Integer - views: Integer - clicks: Integer - participants: Integer - conversions: Integer } class ChannelPerformance { - engagementRate: Double - clickThroughRate: Double - conversionRate: Double - participationRate: Double } class ChannelCosts { - distributionCost: BigDecimal - costPerImpression: BigDecimal - costPerClick: BigDecimal - costPerParticipant: BigDecimal } class ChannelComparison { - bestPerformingChannel: String - mostCostEffectiveChannel: String - highestEngagementChannel: String } class VoiceCallStats { - totalCalls: Integer - completedCalls: Integer - completionRate: Double - averageDuration: Integer } class RoiAnalyticsResponse { - eventId: String - eventTitle: String - roiCalculation: RoiCalculation - investment: InvestmentDetails - revenue: RevenueDetails - costEfficiency: CostEfficiency - projection: RevenueProjection - lastUpdatedAt: LocalDateTime } class RoiCalculation { - currentRoi: Double - targetRoi: Double - achievementRate: Double - breakEvenPoint: BigDecimal } class InvestmentDetails { - totalInvestment: BigDecimal - channelDistribution: Map - costPerChannel: Map } class RevenueDetails { - expectedRevenue: BigDecimal - currentRevenue: BigDecimal - salesGrowthRate: Double } class CostEfficiency { - costPerParticipant: BigDecimal - costPerConversion: BigDecimal - costPerView: BigDecimal } class RevenueProjection { - projectedRevenue: BigDecimal - projectedRoi: Double - estimatedGrowth: Double } class TimelineAnalyticsResponse { - eventId: String - eventTitle: String - granularity: String - period: PeriodInfo - dataPoints: List - trend: TrendAnalysis - peakTime: PeakTimeInfo - lastUpdatedAt: LocalDateTime } class TimelineDataPoint { - timestamp: LocalDateTime - participants: Integer - views: Integer - engagement: Integer - conversions: Integer - cumulativeParticipants: Integer } class TrendAnalysis { - growthRate: Double - averageParticipantsPerHour: Double - totalEngagement: Integer - conversionTrend: String } class PeakTimeInfo { - peakTimestamp: LocalDateTime - peakParticipants: Integer - peakHour: Integer } class UserAnalyticsDashboardResponse { - userId: String - totalEvents: Integer - period: PeriodInfo - summary: AnalyticsSummary - events: List - lastUpdatedAt: LocalDateTime } class UserChannelAnalyticsResponse { - userId: String - totalEvents: Integer - channels: List - comparison: ChannelComparison - lastUpdatedAt: LocalDateTime } class UserRoiAnalyticsResponse { - userId: String - totalEvents: Integer - roiCalculation: RoiCalculation - investment: InvestmentDetails - revenue: RevenueDetails - lastUpdatedAt: LocalDateTime } class UserTimelineAnalyticsResponse { - userId: String - totalEvents: Integer - granularity: String - period: PeriodInfo - dataPoints: List - lastUpdatedAt: LocalDateTime } } ' ============================================================ ' Messaging Layer - Kafka Consumer ' ============================================================ package "com.kt.event.analytics.messaging.consumer" <> #FFE4E1 { class EventCreatedConsumer { - eventStatsRepository: EventStatsRepository - objectMapper: ObjectMapper - redisTemplate: RedisTemplate + handleEventCreated(message: String): void } class ParticipantRegisteredConsumer { - eventStatsRepository: EventStatsRepository - timelineDataRepository: TimelineDataRepository - objectMapper: ObjectMapper - redisTemplate: RedisTemplate + handleParticipantRegistered(message: String): void - updateTimelineData(eventId: String): void } class DistributionCompletedConsumer { - channelStatsRepository: ChannelStatsRepository - objectMapper: ObjectMapper - redisTemplate: RedisTemplate + handleDistributionCompleted(message: String): void } } package "com.kt.event.analytics.messaging.event" <> #FFE4E1 { class EventCreatedEvent { - eventId: String - eventTitle: String - storeId: String - totalInvestment: BigDecimal - status: String } class ParticipantRegisteredEvent { - eventId: String - participantId: String - channelName: String - registeredAt: LocalDateTime } class DistributionCompletedEvent { - eventId: String - channelName: String - distributionCost: BigDecimal - estimatedReach: Integer - completedAt: LocalDateTime } } ' ============================================================ ' Batch Layer ' ============================================================ package "com.kt.event.analytics.batch" <> #FFF5EE { class AnalyticsBatchScheduler { - analyticsService: AnalyticsService - eventStatsRepository: EventStatsRepository - redisTemplate: RedisTemplate + refreshAnalyticsDashboard(): void + initialDataLoad(): void } } ' ============================================================ ' Configuration Layer ' ============================================================ package "com.kt.event.analytics.config" <> #F5F5F5 { class RedisConfig { + redisConnectionFactory(): RedisConnectionFactory + redisTemplate(): RedisTemplate } class KafkaConsumerConfig { + consumerFactory(): ConsumerFactory + kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory } class KafkaTopicConfig { + sampleEventCreatedTopic(): NewTopic + sampleParticipantRegisteredTopic(): NewTopic + sampleDistributionCompletedTopic(): NewTopic } class Resilience4jConfig { + customize(factory: Resilience4JCircuitBreakerFactory): Customizer } class SecurityConfig { + securityFilterChain(http: HttpSecurity): SecurityFilterChain } class SwaggerConfig { + openAPI(): OpenAPI } } ' ============================================================ ' Common Components (from common-base) ' ============================================================ package "com.kt.event.common" <> #DCDCDC { abstract class BaseTimeEntity { - createdAt: LocalDateTime - updatedAt: LocalDateTime } class "ApiResponse" { - success: boolean - data: T - errorCode: String - message: String - timestamp: LocalDateTime + success(data: T): ApiResponse + success(): ApiResponse + error(errorCode: String, message: String): ApiResponse } interface ErrorCode { + getCode(): String + getMessage(): String } class BusinessException { - errorCode: ErrorCode - details: String } } ' ============================================================ ' Relationships ' ============================================================ ' Controller -> Service AnalyticsDashboardController --> AnalyticsService : uses ChannelAnalyticsController --> ChannelAnalyticsService : uses RoiAnalyticsController --> RoiAnalyticsService : uses TimelineAnalyticsController --> TimelineAnalyticsService : uses UserAnalyticsDashboardController --> UserAnalyticsService : uses UserChannelAnalyticsController --> UserChannelAnalyticsService : uses UserRoiAnalyticsController --> UserRoiAnalyticsService : uses UserTimelineAnalyticsController --> UserTimelineAnalyticsService : uses ' Service -> Repository AnalyticsService --> EventStatsRepository : uses AnalyticsService --> ChannelStatsRepository : uses ChannelAnalyticsService --> ChannelStatsRepository : uses RoiAnalyticsService --> EventStatsRepository : uses RoiAnalyticsService --> ChannelStatsRepository : uses TimelineAnalyticsService --> TimelineDataRepository : uses TimelineAnalyticsService --> EventStatsRepository : uses UserAnalyticsService --> EventStatsRepository : uses UserAnalyticsService --> ChannelStatsRepository : uses UserChannelAnalyticsService --> ChannelStatsRepository : uses UserChannelAnalyticsService --> EventStatsRepository : uses UserRoiAnalyticsService --> EventStatsRepository : uses UserRoiAnalyticsService --> ChannelStatsRepository : uses UserTimelineAnalyticsService --> TimelineDataRepository : uses UserTimelineAnalyticsService --> EventStatsRepository : uses ' Service -> Service AnalyticsService --> ExternalChannelService : uses AnalyticsService --> ROICalculator : uses ChannelAnalyticsService --> ExternalChannelService : uses RoiAnalyticsService --> ROICalculator : uses UserAnalyticsService --> ROICalculator : uses UserRoiAnalyticsService --> ROICalculator : uses ' Service -> External Components ExternalChannelService --> ChannelStats : modifies ' Consumer -> Repository EventCreatedConsumer --> EventStatsRepository : uses ParticipantRegisteredConsumer --> EventStatsRepository : uses ParticipantRegisteredConsumer --> TimelineDataRepository : uses DistributionCompletedConsumer --> ChannelStatsRepository : uses ' Consumer -> Event EventCreatedConsumer --> EventCreatedEvent : consumes ParticipantRegisteredConsumer --> ParticipantRegisteredEvent : consumes DistributionCompletedConsumer --> DistributionCompletedEvent : consumes ' Batch -> Service/Repository AnalyticsBatchScheduler --> AnalyticsService : uses AnalyticsBatchScheduler --> EventStatsRepository : uses ' Repository -> Entity EventStatsRepository --> EventStats : manages ChannelStatsRepository --> ChannelStats : manages TimelineDataRepository --> TimelineData : manages ' Entity -> BaseTimeEntity EventStats --|> BaseTimeEntity : extends ChannelStats --|> BaseTimeEntity : extends TimelineData --|> BaseTimeEntity : extends ' Controller -> DTO AnalyticsDashboardController --> AnalyticsDashboardResponse : returns ChannelAnalyticsController --> ChannelAnalyticsResponse : returns RoiAnalyticsController --> RoiAnalyticsResponse : returns TimelineAnalyticsController --> TimelineAnalyticsResponse : returns UserAnalyticsDashboardController --> UserAnalyticsDashboardResponse : returns UserChannelAnalyticsController --> UserChannelAnalyticsResponse : returns UserRoiAnalyticsController --> UserRoiAnalyticsResponse : returns UserTimelineAnalyticsController --> UserTimelineAnalyticsResponse : returns ' Service -> DTO AnalyticsService --> AnalyticsDashboardResponse : creates ChannelAnalyticsService --> ChannelAnalyticsResponse : creates RoiAnalyticsService --> RoiAnalyticsResponse : creates TimelineAnalyticsService --> TimelineAnalyticsResponse : creates UserAnalyticsService --> UserAnalyticsDashboardResponse : creates UserChannelAnalyticsService --> UserChannelAnalyticsResponse : creates UserRoiAnalyticsService --> UserRoiAnalyticsResponse : creates UserTimelineAnalyticsService --> UserTimelineAnalyticsResponse : creates ' DTO Composition AnalyticsDashboardResponse *-- PeriodInfo : contains AnalyticsDashboardResponse *-- AnalyticsSummary : contains AnalyticsDashboardResponse *-- RoiSummary : contains AnalyticsSummary *-- SocialInteractionStats : contains ChannelAnalyticsResponse *-- ChannelAnalytics : contains ChannelAnalyticsResponse *-- ChannelComparison : contains ChannelAnalytics *-- ChannelMetrics : contains ChannelAnalytics *-- ChannelPerformance : contains ChannelAnalytics *-- ChannelCosts : contains ChannelAnalytics *-- VoiceCallStats : contains RoiAnalyticsResponse *-- RoiCalculation : contains RoiAnalyticsResponse *-- InvestmentDetails : contains RoiAnalyticsResponse *-- RevenueDetails : contains RoiAnalyticsResponse *-- CostEfficiency : contains RoiAnalyticsResponse *-- RevenueProjection : contains TimelineAnalyticsResponse *-- PeriodInfo : contains TimelineAnalyticsResponse *-- TimelineDataPoint : contains TimelineAnalyticsResponse *-- TrendAnalysis : contains TimelineAnalyticsResponse *-- PeakTimeInfo : contains UserAnalyticsDashboardResponse *-- PeriodInfo : contains UserAnalyticsDashboardResponse *-- AnalyticsSummary : contains ' Common Dependencies BusinessException --> ErrorCode : uses AnalyticsDashboardController --> "ApiResponse" : uses ChannelAnalyticsController --> "ApiResponse" : uses RoiAnalyticsController --> "ApiResponse" : uses TimelineAnalyticsController --> "ApiResponse" : uses note top of AnalyticsService **핵심 서비스** - Redis 캐싱 (1시간 TTL) - 외부 API 병렬 호출 - Circuit Breaker 패턴 - Cache-Aside 패턴 end note note top of ExternalChannelService **외부 채널 통합** - 우리동네TV API - 지니TV API - 링고비즈 API - SNS APIs - Resilience4j Circuit Breaker end note note top of EventCreatedConsumer **Kafka Event Consumer** - EventCreated 이벤트 구독 - 멱등성 처리 (Redis Set) - 캐시 무효화 end note note top of AnalyticsBatchScheduler **배치 스케줄러** - 5분 단위 캐시 갱신 - 초기 데이터 로딩 - 캐시 워밍업 end note @enduml