@startuml
!theme mono

title AI Service 클래스 다이어그램 (Clean Architecture)

' ===== Presentation Layer (Interface Adapters) =====
package "com.kt.ai.controller" <<Rectangle>> #E8F5E9 {
    class HealthController {
        + checkHealth(): ResponseEntity<HealthCheckResponse>
        - getServiceStatus(): ServiceStatus
        - checkRedisConnection(): boolean
    }

    class InternalRecommendationController {
        - aiRecommendationService: AIRecommendationService
        - cacheService: CacheService
        - redisTemplate: RedisTemplate<String, Object>
        + getRecommendation(eventId: String): ResponseEntity<AIRecommendationResult>
        + debugRedisKeys(): ResponseEntity<Map<String, Object>>
        + debugRedisKey(key: String): ResponseEntity<Map<String, Object>>
        + searchAllDatabases(): ResponseEntity<Map<String, Object>>
        + createTestData(eventId: String): ResponseEntity<Map<String, Object>>
    }

    class InternalJobController {
        - jobStatusService: JobStatusService
        - cacheService: CacheService
        + getJobStatus(jobId: String): ResponseEntity<JobStatusResponse>
        + createTestJob(jobId: String): ResponseEntity<Map<String, Object>>
    }
}

' ===== Application Layer (Use Cases) =====
package "com.kt.ai.service" <<Rectangle>> #FFF9C4 {
    class AIRecommendationService {
        - cacheService: CacheService
        - jobStatusService: JobStatusService
        - trendAnalysisService: TrendAnalysisService
        - claudeApiClient: ClaudeApiClient
        - circuitBreakerManager: CircuitBreakerManager
        - fallback: AIServiceFallback
        - objectMapper: ObjectMapper
        - aiProvider: String
        - apiKey: String
        - anthropicVersion: String
        - model: String
        - maxTokens: Integer
        - temperature: Double
        + getRecommendation(eventId: String): AIRecommendationResult
        + generateRecommendations(message: AIJobMessage): void
        - analyzeTrend(message: AIJobMessage): TrendAnalysis
        - createRecommendations(message: AIJobMessage, trendAnalysis: TrendAnalysis): List<EventRecommendation>
        - callClaudeApiForRecommendations(message: AIJobMessage, trendAnalysis: TrendAnalysis): List<EventRecommendation>
        - buildRecommendationPrompt(message: AIJobMessage, trendAnalysis: TrendAnalysis): String
        - parseRecommendationResponse(responseText: String): List<EventRecommendation>
        - parseEventRecommendation(node: JsonNode): EventRecommendation
        - parseRange(node: JsonNode): ExpectedMetrics.Range
        - extractJsonFromMarkdown(text: String): String
    }

    class TrendAnalysisService {
        - claudeApiClient: ClaudeApiClient
        - circuitBreakerManager: CircuitBreakerManager
        - fallback: AIServiceFallback
        - objectMapper: ObjectMapper
        - apiKey: String
        - anthropicVersion: String
        - model: String
        - maxTokens: Integer
        - temperature: Double
        + analyzeTrend(industry: String, region: String): TrendAnalysis
        - callClaudeApi(industry: String, region: String): TrendAnalysis
        - buildPrompt(industry: String, region: String): String
        - parseResponse(responseText: String): TrendAnalysis
        - extractJsonFromMarkdown(text: String): String
        - parseTrendKeywords(arrayNode: JsonNode): List<TrendAnalysis.TrendKeyword>
    }

    class JobStatusService {
        - cacheService: CacheService
        - objectMapper: ObjectMapper
        + getJobStatus(jobId: String): JobStatusResponse
        + updateJobStatus(jobId: String, status: JobStatus, message: String): void
        - calculateProgress(status: JobStatus): int
    }

    class CacheService {
        - redisTemplate: RedisTemplate<String, Object>
        - recommendationTtl: long
        - jobStatusTtl: long
        - trendTtl: long
        + set(key: String, value: Object, ttlSeconds: long): void
        + get(key: String): Object
        + delete(key: String): void
        + saveJobStatus(jobId: String, status: Object): void
        + getJobStatus(jobId: String): Object
        + saveRecommendation(eventId: String, recommendation: Object): void
        + getRecommendation(eventId: String): Object
        + saveTrend(industry: String, region: String, trend: Object): void
        + getTrend(industry: String, region: String): Object
    }
}

' ===== Domain Layer (Entities & Business Logic) =====
package "com.kt.ai.model" <<Rectangle>> #E1BEE7 {
    package "dto.response" {
        class AIRecommendationResult {
            - eventId: String
            - trendAnalysis: TrendAnalysis
            - recommendations: List<EventRecommendation>
            - generatedAt: LocalDateTime
            - expiresAt: LocalDateTime
            - aiProvider: AIProvider
        }

        class TrendAnalysis {
            - industryTrends: List<TrendKeyword>
            - regionalTrends: List<TrendKeyword>
            - seasonalTrends: List<TrendKeyword>
        }

        class "TrendAnalysis.TrendKeyword" as TrendKeyword {
            - keyword: String
            - relevance: Double
            - description: String
        }

        class EventRecommendation {
            - optionNumber: Integer
            - concept: String
            - title: String
            - description: String
            - targetAudience: String
            - duration: Duration
            - mechanics: Mechanics
            - promotionChannels: List<String>
            - estimatedCost: EstimatedCost
            - expectedMetrics: ExpectedMetrics
            - differentiator: String
        }

        class "EventRecommendation.Duration" as Duration {
            - recommendedDays: Integer
            - recommendedPeriod: String
        }

        class "EventRecommendation.Mechanics" as Mechanics {
            - type: EventMechanicsType
            - details: String
        }

        class "EventRecommendation.EstimatedCost" as EstimatedCost {
            - min: Integer
            - max: Integer
            - breakdown: Map<String, Integer>
        }

        class ExpectedMetrics {
            - newCustomers: Range
            - revenueIncrease: Range
            - roi: Range
        }

        class "ExpectedMetrics.Range" as Range {
            - min: Double
            - max: Double
        }

        class JobStatusResponse {
            - jobId: String
            - status: JobStatus
            - progress: Integer
            - message: String
            - createdAt: LocalDateTime
        }

        class HealthCheckResponse {
            - status: ServiceStatus
            - timestamp: LocalDateTime
            - redisConnected: boolean
        }

        class ErrorResponse {
            - success: boolean
            - errorCode: String
            - message: String
            - timestamp: LocalDateTime
            - details: Map<String, Object>
        }
    }

    package "enums" {
        enum AIProvider {
            CLAUDE
            GPT_4
        }

        enum JobStatus {
            PENDING
            PROCESSING
            COMPLETED
            FAILED
        }

        enum EventMechanicsType {
            DISCOUNT
            GIFT
            STAMP
            EXPERIENCE
            LOTTERY
            COMBO
        }

        enum ServiceStatus {
            UP
            DOWN
            DEGRADED
        }

        enum CircuitBreakerState {
            CLOSED
            OPEN
            HALF_OPEN
        }
    }
}

' ===== Infrastructure Layer (External Interfaces) =====
package "com.kt.ai.client" <<Rectangle>> #FFCCBC {
    interface ClaudeApiClient {
        + sendMessage(apiKey: String, anthropicVersion: String, request: ClaudeRequest): ClaudeResponse
    }

    package "dto" {
        class ClaudeRequest {
            - model: String
            - messages: List<Message>
            - maxTokens: Integer
            - temperature: Double
            - system: String
        }

        class "ClaudeRequest.Message" as Message {
            - role: String
            - content: String
        }

        class ClaudeResponse {
            - id: String
            - type: String
            - role: String
            - content: List<Content>
            - model: String
            - stopReason: String
            - stopSequence: String
            - usage: Usage
            + extractText(): String
        }

        class "ClaudeResponse.Content" as Content {
            - type: String
            - text: String
        }

        class "ClaudeResponse.Usage" as Usage {
            - inputTokens: Integer
            - outputTokens: Integer
        }
    }

    package "config" {
        class FeignClientConfig {
            + feignEncoder(): Encoder
            + feignDecoder(): Decoder
            + feignLoggerLevel(): Logger.Level
            + feignErrorDecoder(): ErrorDecoder
        }
    }
}

package "com.kt.ai.circuitbreaker" <<Rectangle>> #FFCCBC {
    class CircuitBreakerManager {
        - circuitBreakerRegistry: CircuitBreakerRegistry
        + executeWithCircuitBreaker(circuitBreakerName: String, supplier: Supplier<T>, fallback: Supplier<T>): T
        + executeWithCircuitBreaker(circuitBreakerName: String, supplier: Supplier<T>): T
        + getCircuitBreakerState(circuitBreakerName: String): CircuitBreaker.State
    }

    package "fallback" {
        class AIServiceFallback {
            + getDefaultTrendAnalysis(industry: String, region: String): TrendAnalysis
            + getDefaultRecommendations(objective: String, industry: String): List<EventRecommendation>
            - createDefaultTrendKeyword(keyword: String, relevance: Double, description: String): TrendAnalysis.TrendKeyword
            - createDefaultRecommendation(optionNumber: Integer, concept: String, title: String): EventRecommendation
        }
    }
}

package "com.kt.ai.kafka" <<Rectangle>> #FFCCBC {
    package "consumer" {
        class AIJobConsumer {
            - aiRecommendationService: AIRecommendationService
            - objectMapper: ObjectMapper
            + consumeAIJobMessage(message: String): void
            - parseAIJobMessage(message: String): AIJobMessage
        }
    }

    package "message" {
        class AIJobMessage {
            - jobId: String
            - eventId: String
            - storeName: String
            - industry: String
            - region: String
            - objective: String
            - targetAudience: String
            - budget: Integer
            - requestedAt: LocalDateTime
        }
    }
}

' ===== Exception Layer =====
package "com.kt.ai.exception" <<Rectangle>> #FFEBEE {
    class GlobalExceptionHandler {
        + handleBusinessException(e: BusinessException): ResponseEntity<ErrorResponse>
        + handleJobNotFoundException(e: JobNotFoundException): ResponseEntity<ErrorResponse>
        + handleRecommendationNotFoundException(e: RecommendationNotFoundException): ResponseEntity<ErrorResponse>
        + handleCircuitBreakerOpenException(e: CircuitBreakerOpenException): ResponseEntity<ErrorResponse>
        + handleAIServiceException(e: AIServiceException): ResponseEntity<ErrorResponse>
        + handleException(e: Exception): ResponseEntity<ErrorResponse>
        - buildErrorResponse(errorCode: String, message: String): ErrorResponse
    }

    class AIServiceException {
        - errorCode: String
        - details: String
        + AIServiceException(message: String)
        + AIServiceException(message: String, cause: Throwable)
        + AIServiceException(errorCode: String, message: String)
        + AIServiceException(errorCode: String, message: String, details: String)
    }

    class JobNotFoundException {
        - jobId: String
        + JobNotFoundException(jobId: String)
        + JobNotFoundException(jobId: String, cause: Throwable)
    }

    class RecommendationNotFoundException {
        - eventId: String
        + RecommendationNotFoundException(eventId: String)
        + RecommendationNotFoundException(eventId: String, cause: Throwable)
    }

    class CircuitBreakerOpenException {
        - circuitBreakerName: String
        + CircuitBreakerOpenException(circuitBreakerName: String)
        + CircuitBreakerOpenException(circuitBreakerName: String, cause: Throwable)
    }
}

' ===== Configuration Layer =====
package "com.kt.ai.config" <<Rectangle>> #E3F2FD {
    class SecurityConfig {
        + securityFilterChain(http: HttpSecurity): SecurityFilterChain
        + passwordEncoder(): PasswordEncoder
    }

    class RedisConfig {
        - host: String
        - port: int
        - password: String
        + redisConnectionFactory(): LettuceConnectionFactory
        + redisTemplate(): RedisTemplate<String, Object>
    }

    class CircuitBreakerConfig {
        + circuitBreakerRegistry(): CircuitBreakerRegistry
        + circuitBreakerConfigCustomizer(): Customizer<CircuitBreakerConfigurationProperties.InstanceProperties>
    }

    class KafkaConsumerConfig {
        - bootstrapServers: String
        - groupId: String
        + consumerFactory(): ConsumerFactory<String, String>
        + kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String>
    }

    class JacksonConfig {
        + objectMapper(): ObjectMapper
    }

    class SwaggerConfig {
        + openAPI(): OpenAPI
    }
}

' ===== Main Application =====
package "com.kt.ai" <<Rectangle>> #F3E5F5 {
    class AiServiceApplication {
        + {static} main(args: String[]): void
    }
}

' ===== 관계 정의 =====

' Controller → Service
InternalRecommendationController --> AIRecommendationService : uses
InternalRecommendationController --> CacheService : uses
InternalJobController --> JobStatusService : uses
InternalJobController --> CacheService : uses

' Service → Service
AIRecommendationService --> TrendAnalysisService : uses
AIRecommendationService --> JobStatusService : uses
AIRecommendationService --> CacheService : uses
JobStatusService --> CacheService : uses

' Service → Client
AIRecommendationService --> ClaudeApiClient : uses
TrendAnalysisService --> ClaudeApiClient : uses

' Service → CircuitBreaker
AIRecommendationService --> CircuitBreakerManager : uses
AIRecommendationService --> AIServiceFallback : uses
TrendAnalysisService --> CircuitBreakerManager : uses
TrendAnalysisService --> AIServiceFallback : uses

' Kafka → Service
AIJobConsumer --> AIRecommendationService : uses
AIJobConsumer --> AIJobMessage : uses

' Service → Domain
AIRecommendationService --> AIRecommendationResult : creates
AIRecommendationService --> EventRecommendation : creates
TrendAnalysisService --> TrendAnalysis : creates
JobStatusService --> JobStatusResponse : creates

' Domain Relationships
AIRecommendationResult *-- TrendAnalysis
AIRecommendationResult *-- EventRecommendation
AIRecommendationResult --> AIProvider
TrendAnalysis *-- TrendKeyword
EventRecommendation *-- Duration
EventRecommendation *-- Mechanics
EventRecommendation *-- EstimatedCost
EventRecommendation *-- ExpectedMetrics
Mechanics --> EventMechanicsType
ExpectedMetrics *-- Range
JobStatusResponse --> JobStatus
HealthCheckResponse --> ServiceStatus

' Client Relationships
ClaudeApiClient --> ClaudeRequest : uses
ClaudeApiClient --> ClaudeResponse : returns
ClaudeRequest *-- Message
ClaudeResponse *-- Content
ClaudeResponse *-- Usage

' Exception Relationships
GlobalExceptionHandler ..> ErrorResponse : creates
GlobalExceptionHandler ..> AIServiceException : handles
GlobalExceptionHandler ..> JobNotFoundException : handles
GlobalExceptionHandler ..> RecommendationNotFoundException : handles
GlobalExceptionHandler ..> CircuitBreakerOpenException : handles

AIServiceException <|-- JobNotFoundException
AIServiceException <|-- RecommendationNotFoundException
AIServiceException <|-- CircuitBreakerOpenException

note top of AiServiceApplication
  Spring Boot Application Entry Point
  - @SpringBootApplication
  - @EnableFeignClients
  - @EnableKafka
end note

note top of AIRecommendationService
  **Use Case (Application Layer)**
  - AI 추천 생성 비즈니스 로직
  - 트렌드 분석 → 추천안 생성
  - Circuit Breaker 적용
  - Redis 캐싱 전략
end note

note top of TrendAnalysisService
  **Use Case (Application Layer)**
  - Claude AI를 통한 트렌드 분석
  - 업종/지역/계절 트렌드
  - Circuit Breaker Fallback
end note

note top of ClaudeApiClient
  **Infrastructure (External Interface)**
  - Feign Client
  - Claude API 연동
  - HTTP 통신 처리
end note

note top of CircuitBreakerManager
  **Infrastructure (Resilience)**
  - Resilience4j Circuit Breaker
  - 외부 API 장애 격리
  - Fallback 메커니즘
end note

note top of CacheService
  **Infrastructure (Data Access)**
  - Redis 캐싱 인프라
  - TTL 관리
  - 추천/트렌드/상태 캐싱
end note

note right of AIRecommendationResult
  **Domain Entity**
  - AI 추천 결과
  - 불변 객체 (Immutable)
  - Builder 패턴
end note

note right of TrendAnalysis
  **Domain Entity**
  - 트렌드 분석 결과
  - 업종/지역/계절별 구분
end note

@enduml
