mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2026-06-13 14:19:11 +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>
This commit is contained in:
@@ -0,0 +1,579 @@
|
||||
@startuml
|
||||
!theme mono
|
||||
|
||||
title Event Service 클래스 다이어그램 (상세)
|
||||
|
||||
' ==============================
|
||||
' Domain Layer (핵심 비즈니스 로직)
|
||||
' ==============================
|
||||
package "com.kt.event.eventservice.domain" {
|
||||
|
||||
package "entity" {
|
||||
class Event extends BaseTimeEntity {
|
||||
- eventId: UUID
|
||||
- userId: UUID
|
||||
- storeId: UUID
|
||||
- eventName: String
|
||||
- description: String
|
||||
- objective: String
|
||||
- startDate: LocalDate
|
||||
- endDate: LocalDate
|
||||
- status: EventStatus
|
||||
- selectedImageId: UUID
|
||||
- selectedImageUrl: String
|
||||
- channels: List<String>
|
||||
- generatedImages: Set<GeneratedImage>
|
||||
- aiRecommendations: Set<AiRecommendation>
|
||||
|
||||
' 비즈니스 로직
|
||||
+ updateEventName(eventName: String): void
|
||||
+ updateDescription(description: String): void
|
||||
+ updateEventPeriod(startDate: LocalDate, endDate: LocalDate): void
|
||||
+ selectImage(imageId: UUID, imageUrl: String): void
|
||||
+ updateChannels(channels: List<String>): void
|
||||
+ publish(): void
|
||||
+ end(): void
|
||||
+ addGeneratedImage(image: GeneratedImage): void
|
||||
+ addAiRecommendation(recommendation: AiRecommendation): void
|
||||
+ isModifiable(): boolean
|
||||
+ isDeletable(): boolean
|
||||
}
|
||||
|
||||
class AiRecommendation extends BaseTimeEntity {
|
||||
- recommendationId: UUID
|
||||
- event: Event
|
||||
- eventName: String
|
||||
- description: String
|
||||
- promotionType: String
|
||||
- targetAudience: String
|
||||
- isSelected: boolean
|
||||
}
|
||||
|
||||
class GeneratedImage extends BaseTimeEntity {
|
||||
- imageId: UUID
|
||||
- event: Event
|
||||
- imageUrl: String
|
||||
- style: String
|
||||
- platform: String
|
||||
- isSelected: boolean
|
||||
}
|
||||
|
||||
class Job extends BaseTimeEntity {
|
||||
- jobId: UUID
|
||||
- eventId: UUID
|
||||
- jobType: JobType
|
||||
- status: JobStatus
|
||||
- progress: int
|
||||
- resultKey: String
|
||||
- errorMessage: String
|
||||
- completedAt: LocalDateTime
|
||||
|
||||
' 비즈니스 로직
|
||||
+ start(): void
|
||||
+ updateProgress(progress: int): void
|
||||
+ complete(resultKey: String): void
|
||||
+ fail(errorMessage: String): void
|
||||
}
|
||||
}
|
||||
|
||||
package "enums" {
|
||||
enum EventStatus {
|
||||
DRAFT
|
||||
PUBLISHED
|
||||
ENDED
|
||||
}
|
||||
|
||||
enum JobStatus {
|
||||
PENDING
|
||||
PROCESSING
|
||||
COMPLETED
|
||||
FAILED
|
||||
}
|
||||
|
||||
enum JobType {
|
||||
AI_RECOMMENDATION
|
||||
IMAGE_GENERATION
|
||||
}
|
||||
}
|
||||
|
||||
package "repository" {
|
||||
interface EventRepository extends JpaRepository {
|
||||
+ findByEventIdAndUserId(eventId: UUID, userId: UUID): Optional<Event>
|
||||
+ findEventsByUser(userId: UUID, status: EventStatus, search: String, objective: String, pageable: Pageable): Page<Event>
|
||||
}
|
||||
|
||||
interface AiRecommendationRepository extends JpaRepository {
|
||||
+ findByEvent(event: Event): List<AiRecommendation>
|
||||
}
|
||||
|
||||
interface GeneratedImageRepository extends JpaRepository {
|
||||
+ findByEvent(event: Event): List<GeneratedImage>
|
||||
}
|
||||
|
||||
interface JobRepository extends JpaRepository {
|
||||
+ findByEventId(eventId: UUID): List<Job>
|
||||
+ findByJobTypeAndStatus(jobType: JobType, status: JobStatus): List<Job>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' ==============================
|
||||
' Application Layer (유스케이스)
|
||||
' ==============================
|
||||
package "com.kt.event.eventservice.application" {
|
||||
|
||||
package "service" {
|
||||
class EventService {
|
||||
- eventRepository: EventRepository
|
||||
- jobRepository: JobRepository
|
||||
- contentServiceClient: ContentServiceClient
|
||||
- aiJobKafkaProducer: AIJobKafkaProducer
|
||||
|
||||
' 이벤트 생명주기 관리
|
||||
+ createEvent(userId: UUID, storeId: UUID, request: SelectObjectiveRequest): EventCreatedResponse
|
||||
+ getEvent(userId: UUID, eventId: UUID): EventDetailResponse
|
||||
+ getEvents(userId: UUID, status: EventStatus, search: String, objective: String, pageable: Pageable): Page<EventDetailResponse>
|
||||
+ deleteEvent(userId: UUID, eventId: UUID): void
|
||||
+ publishEvent(userId: UUID, eventId: UUID): void
|
||||
+ endEvent(userId: UUID, eventId: UUID): void
|
||||
+ updateEvent(userId: UUID, eventId: UUID, request: UpdateEventRequest): EventDetailResponse
|
||||
|
||||
' AI 추천 관리
|
||||
+ requestAiRecommendations(userId: UUID, eventId: UUID, request: AiRecommendationRequest): JobAcceptedResponse
|
||||
+ selectRecommendation(userId: UUID, eventId: UUID, request: SelectRecommendationRequest): void
|
||||
|
||||
' 이미지 관리
|
||||
+ requestImageGeneration(userId: UUID, eventId: UUID, request: ImageGenerationRequest): ImageGenerationResponse
|
||||
+ selectImage(userId: UUID, eventId: UUID, imageId: UUID, request: SelectImageRequest): void
|
||||
+ editImage(userId: UUID, eventId: UUID, imageId: UUID, request: ImageEditRequest): ImageEditResponse
|
||||
|
||||
' 배포 채널 관리
|
||||
+ selectChannels(userId: UUID, eventId: UUID, request: SelectChannelsRequest): void
|
||||
|
||||
' Helper Methods
|
||||
- mapToDetailResponse(event: Event): EventDetailResponse
|
||||
}
|
||||
|
||||
class JobService {
|
||||
- jobRepository: JobRepository
|
||||
|
||||
+ createJob(eventId: UUID, jobType: JobType): Job
|
||||
+ getJobStatus(jobId: UUID): JobStatusResponse
|
||||
+ updateJobProgress(jobId: UUID, progress: int): void
|
||||
+ completeJob(jobId: UUID, resultKey: String): void
|
||||
+ failJob(jobId: UUID, errorMessage: String): void
|
||||
|
||||
- mapToJobStatusResponse(job: Job): JobStatusResponse
|
||||
}
|
||||
}
|
||||
|
||||
package "dto.request" {
|
||||
class SelectObjectiveRequest {
|
||||
- objective: String
|
||||
}
|
||||
|
||||
class AiRecommendationRequest {
|
||||
- storeInfo: StoreInfo
|
||||
|
||||
+ StoreInfo {
|
||||
- storeName: String
|
||||
- category: String
|
||||
- description: String
|
||||
}
|
||||
}
|
||||
|
||||
class SelectRecommendationRequest {
|
||||
- recommendationId: UUID
|
||||
- customizations: Customizations
|
||||
|
||||
+ Customizations {
|
||||
- eventName: String
|
||||
- description: String
|
||||
- startDate: LocalDate
|
||||
- endDate: LocalDate
|
||||
}
|
||||
}
|
||||
|
||||
class ImageGenerationRequest {
|
||||
- styles: List<String>
|
||||
- platforms: List<String>
|
||||
}
|
||||
|
||||
class SelectImageRequest {
|
||||
- imageId: UUID
|
||||
- imageUrl: String
|
||||
}
|
||||
|
||||
class ImageEditRequest {
|
||||
- editInstructions: String
|
||||
}
|
||||
|
||||
class SelectChannelsRequest {
|
||||
- channels: List<String>
|
||||
}
|
||||
|
||||
class UpdateEventRequest {
|
||||
- eventName: String
|
||||
- description: String
|
||||
- startDate: LocalDate
|
||||
- endDate: LocalDate
|
||||
}
|
||||
}
|
||||
|
||||
package "dto.response" {
|
||||
class EventCreatedResponse {
|
||||
- eventId: UUID
|
||||
- status: EventStatus
|
||||
- objective: String
|
||||
- createdAt: LocalDateTime
|
||||
}
|
||||
|
||||
class EventDetailResponse {
|
||||
- eventId: UUID
|
||||
- userId: UUID
|
||||
- storeId: UUID
|
||||
- eventName: String
|
||||
- description: String
|
||||
- objective: String
|
||||
- startDate: LocalDate
|
||||
- endDate: LocalDate
|
||||
- status: EventStatus
|
||||
- selectedImageId: UUID
|
||||
- selectedImageUrl: String
|
||||
- generatedImages: List<GeneratedImageDto>
|
||||
- aiRecommendations: List<AiRecommendationDto>
|
||||
- channels: List<String>
|
||||
- createdAt: LocalDateTime
|
||||
- updatedAt: LocalDateTime
|
||||
|
||||
+ GeneratedImageDto {
|
||||
- imageId: UUID
|
||||
- imageUrl: String
|
||||
- style: String
|
||||
- platform: String
|
||||
- isSelected: boolean
|
||||
- createdAt: LocalDateTime
|
||||
}
|
||||
|
||||
+ AiRecommendationDto {
|
||||
- recommendationId: UUID
|
||||
- eventName: String
|
||||
- description: String
|
||||
- promotionType: String
|
||||
- targetAudience: String
|
||||
- isSelected: boolean
|
||||
}
|
||||
}
|
||||
|
||||
class JobAcceptedResponse {
|
||||
- jobId: UUID
|
||||
- status: JobStatus
|
||||
- message: String
|
||||
}
|
||||
|
||||
class JobStatusResponse {
|
||||
- jobId: UUID
|
||||
- jobType: JobType
|
||||
- status: JobStatus
|
||||
- progress: int
|
||||
- resultKey: String
|
||||
- errorMessage: String
|
||||
- createdAt: LocalDateTime
|
||||
- completedAt: LocalDateTime
|
||||
}
|
||||
|
||||
class ImageGenerationResponse {
|
||||
- jobId: UUID
|
||||
- status: String
|
||||
- message: String
|
||||
- createdAt: LocalDateTime
|
||||
}
|
||||
|
||||
class ImageEditResponse {
|
||||
- imageId: UUID
|
||||
- imageUrl: String
|
||||
- editedAt: LocalDateTime
|
||||
}
|
||||
}
|
||||
|
||||
package "dto.kafka" {
|
||||
class AIEventGenerationJobMessage {
|
||||
- jobId: String
|
||||
- userId: Long
|
||||
- status: String
|
||||
- createdAt: LocalDateTime
|
||||
- errorMessage: String
|
||||
}
|
||||
|
||||
class EventCreatedMessage {
|
||||
- eventId: String
|
||||
- userId: Long
|
||||
- storeId: Long
|
||||
- objective: String
|
||||
- status: String
|
||||
- createdAt: LocalDateTime
|
||||
}
|
||||
|
||||
class ImageGenerationJobMessage {
|
||||
- jobId: String
|
||||
- eventId: String
|
||||
- styles: List<String>
|
||||
- platforms: List<String>
|
||||
- status: String
|
||||
- createdAt: LocalDateTime
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' ==============================
|
||||
' Infrastructure Layer (기술 구현)
|
||||
' ==============================
|
||||
package "com.kt.event.eventservice.infrastructure" {
|
||||
|
||||
package "kafka" {
|
||||
class AIJobKafkaProducer {
|
||||
- kafkaTemplate: KafkaTemplate<String, Object>
|
||||
- aiEventGenerationJobTopic: String
|
||||
|
||||
+ publishAIGenerationJob(jobId: String, userId: Long, eventId: String, storeName: String, storeCategory: String, storeDescription: String, objective: String): void
|
||||
+ publishMessage(message: AIEventGenerationJobMessage): void
|
||||
}
|
||||
|
||||
class AIJobKafkaConsumer {
|
||||
- objectMapper: ObjectMapper
|
||||
|
||||
+ consumeAIEventGenerationJob(payload: String, partition: int, offset: long, acknowledgment: Acknowledgment): void
|
||||
- processAIEventGenerationJob(message: AIEventGenerationJobMessage): void
|
||||
}
|
||||
|
||||
class ImageJobKafkaConsumer {
|
||||
- objectMapper: ObjectMapper
|
||||
|
||||
+ consumeImageGenerationJob(payload: String, partition: int, offset: long, acknowledgment: Acknowledgment): void
|
||||
- processImageGenerationJob(message: ImageGenerationJobMessage): void
|
||||
}
|
||||
|
||||
class EventKafkaProducer {
|
||||
- kafkaTemplate: KafkaTemplate<String, Object>
|
||||
- eventCreatedTopic: String
|
||||
|
||||
+ publishEventCreated(event: Event): void
|
||||
}
|
||||
}
|
||||
|
||||
package "client" {
|
||||
interface ContentServiceClient {
|
||||
+ generateImages(request: ContentImageGenerationRequest): ContentJobResponse
|
||||
}
|
||||
}
|
||||
|
||||
package "client.dto" {
|
||||
class ContentImageGenerationRequest {
|
||||
- eventDraftId: Long
|
||||
- eventTitle: String
|
||||
- eventDescription: String
|
||||
- styles: List<String>
|
||||
- platforms: List<String>
|
||||
}
|
||||
|
||||
class ContentJobResponse {
|
||||
- id: String
|
||||
- status: String
|
||||
- createdAt: LocalDateTime
|
||||
}
|
||||
}
|
||||
|
||||
package "config" {
|
||||
class RedisConfig {
|
||||
- host: String
|
||||
- port: int
|
||||
|
||||
+ redisConnectionFactory(): RedisConnectionFactory
|
||||
+ redisTemplate(): RedisTemplate<String, Object>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' ==============================
|
||||
' Presentation Layer (API 엔드포인트)
|
||||
' ==============================
|
||||
package "com.kt.event.eventservice.presentation" {
|
||||
|
||||
package "controller" {
|
||||
class EventController {
|
||||
- eventService: EventService
|
||||
|
||||
+ selectObjective(request: SelectObjectiveRequest, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<EventCreatedResponse>>
|
||||
+ getEvents(status: EventStatus, search: String, objective: String, page: int, size: int, sort: String, order: String, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<PageResponse<EventDetailResponse>>>
|
||||
+ getEvent(eventId: UUID, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<EventDetailResponse>>
|
||||
+ deleteEvent(eventId: UUID, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<Void>>
|
||||
+ publishEvent(eventId: UUID, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<Void>>
|
||||
+ endEvent(eventId: UUID, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<Void>>
|
||||
+ requestImageGeneration(eventId: UUID, request: ImageGenerationRequest, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<ImageGenerationResponse>>
|
||||
+ selectImage(eventId: UUID, imageId: UUID, request: SelectImageRequest, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<Void>>
|
||||
+ requestAiRecommendations(eventId: UUID, request: AiRecommendationRequest, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<JobAcceptedResponse>>
|
||||
+ selectRecommendation(eventId: UUID, request: SelectRecommendationRequest, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<Void>>
|
||||
+ editImage(eventId: UUID, imageId: UUID, request: ImageEditRequest, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<ImageEditResponse>>
|
||||
+ selectChannels(eventId: UUID, request: SelectChannelsRequest, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<Void>>
|
||||
+ updateEvent(eventId: UUID, request: UpdateEventRequest, userPrincipal: UserPrincipal): ResponseEntity<ApiResponse<EventDetailResponse>>
|
||||
}
|
||||
|
||||
class JobController {
|
||||
- jobService: JobService
|
||||
|
||||
+ getJobStatus(jobId: UUID): ResponseEntity<ApiResponse<JobStatusResponse>>
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
' ==============================
|
||||
' Config Layer (설정)
|
||||
' ==============================
|
||||
package "com.kt.event.eventservice.config" {
|
||||
class SecurityConfig {
|
||||
+ securityFilterChain(http: HttpSecurity): SecurityFilterChain
|
||||
+ corsConfigurationSource(): CorsConfigurationSource
|
||||
}
|
||||
|
||||
class KafkaConfig {
|
||||
+ producerFactory(): ProducerFactory<String, Object>
|
||||
+ kafkaTemplate(): KafkaTemplate<String, Object>
|
||||
+ consumerFactory(): ConsumerFactory<String, String>
|
||||
+ kafkaListenerContainerFactory(): ConcurrentKafkaListenerContainerFactory<String, String>
|
||||
}
|
||||
|
||||
class DevAuthenticationFilter extends OncePerRequestFilter {
|
||||
+ doFilterInternal(request: HttpServletRequest, response: HttpServletResponse, filterChain: FilterChain): void
|
||||
}
|
||||
}
|
||||
|
||||
' ==============================
|
||||
' Common Layer (공통 컴포넌트)
|
||||
' ==============================
|
||||
package "com.kt.event.common" <<external>> {
|
||||
abstract class BaseTimeEntity {
|
||||
- createdAt: LocalDateTime
|
||||
- updatedAt: LocalDateTime
|
||||
}
|
||||
|
||||
class "ApiResponse<T>" {
|
||||
- success: boolean
|
||||
- data: T
|
||||
- errorCode: String
|
||||
- message: String
|
||||
- timestamp: LocalDateTime
|
||||
}
|
||||
|
||||
class "PageResponse<T>" {
|
||||
- content: List<T>
|
||||
- totalElements: long
|
||||
- totalPages: int
|
||||
- number: int
|
||||
- size: int
|
||||
- first: boolean
|
||||
- last: boolean
|
||||
}
|
||||
|
||||
class BusinessException extends RuntimeException {
|
||||
- errorCode: ErrorCode
|
||||
- details: String
|
||||
}
|
||||
|
||||
interface ErrorCode {
|
||||
+ getCode(): String
|
||||
+ getMessage(): String
|
||||
}
|
||||
}
|
||||
|
||||
' ==============================
|
||||
' 관계 정의
|
||||
' ==============================
|
||||
|
||||
' Domain Layer Relationships
|
||||
Event "1" *-- "many" GeneratedImage : contains >
|
||||
Event "1" *-- "many" AiRecommendation : contains >
|
||||
Event ..> EventStatus : uses
|
||||
Job ..> JobType : uses
|
||||
Job ..> JobStatus : uses
|
||||
|
||||
EventRepository ..> Event : manages
|
||||
AiRecommendationRepository ..> AiRecommendation : manages
|
||||
GeneratedImageRepository ..> GeneratedImage : manages
|
||||
JobRepository ..> Job : manages
|
||||
|
||||
' Application Layer Relationships
|
||||
EventService --> EventRepository : uses
|
||||
EventService --> JobRepository : uses
|
||||
EventService --> ContentServiceClient : uses
|
||||
EventService --> AIJobKafkaProducer : uses
|
||||
JobService --> JobRepository : uses
|
||||
|
||||
EventService ..> SelectObjectiveRequest : uses
|
||||
EventService ..> AiRecommendationRequest : uses
|
||||
EventService ..> SelectRecommendationRequest : uses
|
||||
EventService ..> ImageGenerationRequest : uses
|
||||
EventService ..> SelectImageRequest : uses
|
||||
EventService ..> SelectChannelsRequest : uses
|
||||
EventService ..> UpdateEventRequest : uses
|
||||
|
||||
EventService ..> EventCreatedResponse : creates
|
||||
EventService ..> EventDetailResponse : creates
|
||||
EventService ..> JobAcceptedResponse : creates
|
||||
EventService ..> ImageGenerationResponse : creates
|
||||
JobService ..> JobStatusResponse : creates
|
||||
|
||||
' Infrastructure Layer Relationships
|
||||
AIJobKafkaProducer ..> AIEventGenerationJobMessage : publishes
|
||||
AIJobKafkaConsumer ..> AIEventGenerationJobMessage : consumes
|
||||
ImageJobKafkaConsumer ..> ImageGenerationJobMessage : consumes
|
||||
EventKafkaProducer ..> EventCreatedMessage : publishes
|
||||
|
||||
ContentServiceClient ..> ContentImageGenerationRequest : uses
|
||||
ContentServiceClient ..> ContentJobResponse : returns
|
||||
|
||||
' Presentation Layer Relationships
|
||||
EventController --> EventService : uses
|
||||
JobController --> JobService : uses
|
||||
|
||||
EventController ..> ApiResponse : uses
|
||||
EventController ..> PageResponse : uses
|
||||
|
||||
' Common Layer Inheritance
|
||||
Event --|> BaseTimeEntity
|
||||
AiRecommendation --|> BaseTimeEntity
|
||||
GeneratedImage --|> BaseTimeEntity
|
||||
Job --|> BaseTimeEntity
|
||||
|
||||
' Notes
|
||||
note top of Event
|
||||
**핵심 도메인 엔티티**
|
||||
- 이벤트 생명주기 관리 (DRAFT → PUBLISHED → ENDED)
|
||||
- 상태 머신 패턴 적용
|
||||
- 비즈니스 규칙 캡슐화
|
||||
- 불변성 보장 (수정 메서드를 통한 변경)
|
||||
end note
|
||||
|
||||
note top of EventService
|
||||
**핵심 유스케이스 오케스트레이터**
|
||||
- 이벤트 전체 생명주기 조율
|
||||
- AI 서비스 연동 (Kafka)
|
||||
- Content 서비스 연동 (Feign)
|
||||
- 트랜잭션 경계 관리
|
||||
end note
|
||||
|
||||
note top of AIJobKafkaProducer
|
||||
**비동기 작업 발행자**
|
||||
- AI 추천 생성 작업 발행
|
||||
- 장시간 작업의 비동기 처리
|
||||
- Kafka 메시지 발행
|
||||
end note
|
||||
|
||||
note bottom of EventController
|
||||
**REST API 엔드포인트**
|
||||
- 이벤트 생성부터 배포까지 전체 API
|
||||
- 인증/인가 처리
|
||||
- 입력 검증
|
||||
- 표준 응답 포맷 (ApiResponse)
|
||||
end note
|
||||
|
||||
@enduml
|
||||
Reference in New Issue
Block a user