distribution-service API 명세서를 실제 구현에 맞게 수정
- ChannelType 열거형 값 수정 (URIDONGNETV, RINGOBIZ, GINITV 등) - DistributionRequest 스키마 변경 (title, description, imageUrl 추가) - DistributionResponse 스키마 변경 (success, successCount, failureCount 등) - ChannelDistributionResult 스키마 단순화 - 모든 예제 코드 실제 구현에 맞게 업데이트 - IntelliJ 서비스 실행 프로파일 추가 - Distribution 서비스 엔티티, 매퍼, 리포지토리 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
9f50c7feaa
commit
b0d8a6d10e
31
.run/AiServiceApplication.run.xml
Normal file
31
.run/AiServiceApplication.run.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="AiServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||
<option name="ACTIVE_PROFILES" />
|
||||
<module name="kt-event-marketing.ai-service.main" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.ai.AiApplication" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="com.kt.ai.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<envs>
|
||||
<env name="SERVER_PORT" value="8081" />
|
||||
<env name="DB_HOST" value="4.230.112.141" />
|
||||
<env name="DB_PORT" value="5432" />
|
||||
<env name="DB_NAME" value="aidb" />
|
||||
<env name="DB_USERNAME" value="eventuser" />
|
||||
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||
<env name="REDIS_PORT" value="6379" />
|
||||
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="KAFKA_BOOTSTRAP_SERVERS" value="4.230.50.63:9092" />
|
||||
<env name="KAFKA_CONSUMER_GROUP" value="ai" />
|
||||
<env name="JPA_DDL_AUTO" value="update" />
|
||||
<env name="JPA_SHOW_SQL" value="false" />
|
||||
</envs>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
31
.run/AnalyticsServiceApplication.run.xml
Normal file
31
.run/AnalyticsServiceApplication.run.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="AnalyticsServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||
<option name="ACTIVE_PROFILES" />
|
||||
<module name="kt-event-marketing.analytics-service.main" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.analytics.AnalyticsApplication" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="com.kt.analytics.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<envs>
|
||||
<env name="SERVER_PORT" value="8087" />
|
||||
<env name="DB_HOST" value="4.230.49.9" />
|
||||
<env name="DB_PORT" value="5432" />
|
||||
<env name="DB_NAME" value="analyticdb" />
|
||||
<env name="DB_USERNAME" value="eventuser" />
|
||||
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||
<env name="REDIS_PORT" value="6379" />
|
||||
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="KAFKA_BOOTSTRAP_SERVERS" value="4.230.50.63:9092" />
|
||||
<env name="KAFKA_CONSUMER_GROUP" value="analytic" />
|
||||
<env name="JPA_DDL_AUTO" value="update" />
|
||||
<env name="JPA_SHOW_SQL" value="false" />
|
||||
</envs>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
29
.run/ContentServiceApplication.run.xml
Normal file
29
.run/ContentServiceApplication.run.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="ContentServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||
<option name="ACTIVE_PROFILES" />
|
||||
<module name="kt-event-marketing.content-service.main" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.content.ContentApplication" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="com.kt.content.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<envs>
|
||||
<env name="SERVER_PORT" value="8084" />
|
||||
<env name="DB_HOST" value="4.217.131.139" />
|
||||
<env name="DB_PORT" value="5432" />
|
||||
<env name="DB_NAME" value="contentdb" />
|
||||
<env name="DB_USERNAME" value="eventuser" />
|
||||
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||
<env name="REDIS_PORT" value="6379" />
|
||||
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="JPA_DDL_AUTO" value="update" />
|
||||
<env name="JPA_SHOW_SQL" value="false" />
|
||||
</envs>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
31
.run/EventServiceApplication.run.xml
Normal file
31
.run/EventServiceApplication.run.xml
Normal file
@ -0,0 +1,31 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="EventServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||
<option name="ACTIVE_PROFILES" />
|
||||
<module name="kt-event-marketing.event-service.main" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.event.EventApplication" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="com.kt.event.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<envs>
|
||||
<env name="SERVER_PORT" value="8082" />
|
||||
<env name="DB_HOST" value="20.249.177.232" />
|
||||
<env name="DB_PORT" value="5432" />
|
||||
<env name="DB_NAME" value="eventdb" />
|
||||
<env name="DB_USERNAME" value="eventuser" />
|
||||
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||
<env name="REDIS_PORT" value="6379" />
|
||||
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="KAFKA_BOOTSTRAP_SERVERS" value="4.230.50.63:9092" />
|
||||
<env name="DISTRIBUTION_SERVICE_URL" value="http://localhost:8085" />
|
||||
<env name="JPA_DDL_AUTO" value="update" />
|
||||
<env name="JPA_SHOW_SQL" value="false" />
|
||||
</envs>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
29
.run/ParticipationServiceApplication.run.xml
Normal file
29
.run/ParticipationServiceApplication.run.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="ParticipationServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||
<option name="ACTIVE_PROFILES" />
|
||||
<module name="kt-event-marketing.participation-service.main" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.participation.ParticipationApplication" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="com.kt.participation.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<envs>
|
||||
<env name="SERVER_PORT" value="8086" />
|
||||
<env name="DB_HOST" value="4.230.72.147" />
|
||||
<env name="DB_PORT" value="5432" />
|
||||
<env name="DB_NAME" value="participationdb" />
|
||||
<env name="DB_USERNAME" value="eventuser" />
|
||||
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||
<env name="REDIS_PORT" value="6379" />
|
||||
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="JPA_DDL_AUTO" value="update" />
|
||||
<env name="JPA_SHOW_SQL" value="false" />
|
||||
</envs>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
29
.run/UserServiceApplication.run.xml
Normal file
29
.run/UserServiceApplication.run.xml
Normal file
@ -0,0 +1,29 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="UserServiceApplication" type="SpringBootApplicationConfigurationType" factoryName="Spring Boot" nameIsGenerated="true">
|
||||
<option name="ACTIVE_PROFILES" />
|
||||
<module name="kt-event-marketing.user-service.main" />
|
||||
<option name="SPRING_BOOT_MAIN_CLASS" value="com.kt.user.UserApplication" />
|
||||
<extension name="coverage">
|
||||
<pattern>
|
||||
<option name="PATTERN" value="com.kt.user.*" />
|
||||
<option name="ENABLED" value="true" />
|
||||
</pattern>
|
||||
</extension>
|
||||
<envs>
|
||||
<env name="SERVER_PORT" value="8083" />
|
||||
<env name="DB_HOST" value="20.249.125.115" />
|
||||
<env name="DB_PORT" value="5432" />
|
||||
<env name="DB_NAME" value="userdb" />
|
||||
<env name="DB_USERNAME" value="eventuser" />
|
||||
<env name="DB_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="REDIS_HOST" value="20.214.210.71" />
|
||||
<env name="REDIS_PORT" value="6379" />
|
||||
<env name="REDIS_PASSWORD" value="Hi5Jessica!" />
|
||||
<env name="JPA_DDL_AUTO" value="update" />
|
||||
<env name="JPA_SHOW_SQL" value="false" />
|
||||
</envs>
|
||||
<method v="2">
|
||||
<option name="Make" enabled="true" />
|
||||
</method>
|
||||
</configuration>
|
||||
</component>
|
||||
@ -11,10 +11,10 @@ info:
|
||||
- Retry 패턴 및 Fallback 처리
|
||||
|
||||
## 배포 채널
|
||||
- **우리동네TV**: 영상 콘텐츠 업로드
|
||||
- **링고비즈**: 연결음 업데이트
|
||||
- **지니TV**: 광고 등록
|
||||
- **SNS**: Instagram, Naver Blog, Kakao Channel
|
||||
- **우리동네TV** (URIDONGNETV): 영상 콘텐츠 업로드
|
||||
- **링고비즈** (RINGOBIZ): 연결음 업데이트
|
||||
- **지니TV** (GINITV): 광고 등록
|
||||
- **SNS**: Instagram (INSTAGRAM), Naver Blog (NAVER), Kakao Channel (KAKAO)
|
||||
|
||||
## Resilience 패턴
|
||||
- Circuit Breaker: 채널별 독립적 장애 격리
|
||||
@ -79,23 +79,21 @@ paths:
|
||||
summary: 다중 채널 배포 예시
|
||||
value:
|
||||
eventId: "evt-12345"
|
||||
title: "신규 고객 환영 이벤트"
|
||||
description: "신규 고객님을 위한 특별 할인 이벤트"
|
||||
imageUrl: "https://cdn.example.com/images/event-main.jpg"
|
||||
channels:
|
||||
- type: "WOORIDONGNE_TV"
|
||||
config:
|
||||
radius: "1km"
|
||||
timeSlots:
|
||||
- "weekday_evening"
|
||||
- "weekend_lunch"
|
||||
- type: "INSTAGRAM"
|
||||
config:
|
||||
scheduledTime: "2025-11-01T10:00:00Z"
|
||||
- type: "NAVER_BLOG"
|
||||
config:
|
||||
scheduledTime: "2025-11-01T10:30:00Z"
|
||||
contentUrls:
|
||||
instagram: "https://cdn.example.com/images/event-instagram.jpg"
|
||||
naverBlog: "https://cdn.example.com/images/event-naver.jpg"
|
||||
kakaoChannel: "https://cdn.example.com/images/event-kakao.jpg"
|
||||
- "URIDONGNETV"
|
||||
- "INSTAGRAM"
|
||||
- "NAVER"
|
||||
channelSettings:
|
||||
URIDONGNETV:
|
||||
radius: "1km"
|
||||
timeSlot: "evening"
|
||||
INSTAGRAM:
|
||||
scheduledTime: "2025-11-01T10:00:00"
|
||||
NAVER:
|
||||
scheduledTime: "2025-11-01T10:30:00"
|
||||
responses:
|
||||
'200':
|
||||
description: 배포 완료
|
||||
@ -107,25 +105,29 @@ paths:
|
||||
allSuccess:
|
||||
summary: 모든 채널 배포 성공
|
||||
value:
|
||||
distributionId: "dist-12345"
|
||||
eventId: "evt-12345"
|
||||
status: "COMPLETED"
|
||||
completedAt: "2025-11-01T09:00:00Z"
|
||||
results:
|
||||
- channel: "WOORIDONGNE_TV"
|
||||
status: "SUCCESS"
|
||||
success: true
|
||||
channelResults:
|
||||
- channel: "URIDONGNETV"
|
||||
success: true
|
||||
distributionId: "wtv-uuid-12345"
|
||||
estimatedViews: 1000
|
||||
message: "배포 완료"
|
||||
estimatedReach: 1000
|
||||
executionTimeMs: 234
|
||||
- channel: "INSTAGRAM"
|
||||
status: "SUCCESS"
|
||||
postUrl: "https://instagram.com/p/generated-post-id"
|
||||
postId: "ig-post-12345"
|
||||
message: "게시 완료"
|
||||
- channel: "NAVER_BLOG"
|
||||
status: "SUCCESS"
|
||||
postUrl: "https://blog.naver.com/store123/generated-post"
|
||||
message: "게시 완료"
|
||||
success: true
|
||||
distributionId: "ig-uuid-12345"
|
||||
estimatedReach: 500
|
||||
executionTimeMs: 456
|
||||
- channel: "NAVER"
|
||||
success: true
|
||||
distributionId: "naver-uuid-12345"
|
||||
estimatedReach: 300
|
||||
executionTimeMs: 123
|
||||
successCount: 3
|
||||
failureCount: 0
|
||||
completedAt: "2025-11-01T09:00:00"
|
||||
totalExecutionTimeMs: 1234
|
||||
message: "배포가 성공적으로 완료되었습니다"
|
||||
'400':
|
||||
description: 잘못된 요청
|
||||
content:
|
||||
@ -217,67 +219,77 @@ paths:
|
||||
value:
|
||||
eventId: "evt-12345"
|
||||
overallStatus: "COMPLETED"
|
||||
completedAt: "2025-11-01T09:00:00Z"
|
||||
startedAt: "2025-11-01T08:58:00"
|
||||
completedAt: "2025-11-01T09:00:00"
|
||||
channels:
|
||||
- channel: "WOORIDONGNE_TV"
|
||||
- channel: "URIDONGNETV"
|
||||
status: "COMPLETED"
|
||||
distributionId: "wtv-uuid-12345"
|
||||
estimatedViews: 1500
|
||||
completedAt: "2025-11-01T09:00:00Z"
|
||||
- channel: "RINGO_BIZ"
|
||||
completedAt: "2025-11-01T09:00:00"
|
||||
- channel: "RINGOBIZ"
|
||||
status: "COMPLETED"
|
||||
updateTimestamp: "2025-11-01T09:00:00Z"
|
||||
- channel: "GENIE_TV"
|
||||
updateTimestamp: "2025-11-01T09:00:00"
|
||||
completedAt: "2025-11-01T09:00:00"
|
||||
- channel: "GINITV"
|
||||
status: "COMPLETED"
|
||||
adId: "gtv-uuid-12345"
|
||||
impressionSchedule:
|
||||
- "2025-11-01 18:00-20:00"
|
||||
- "2025-11-02 12:00-14:00"
|
||||
completedAt: "2025-11-01T09:00:00"
|
||||
- channel: "INSTAGRAM"
|
||||
status: "COMPLETED"
|
||||
postUrl: "https://instagram.com/p/generated-post-id"
|
||||
postId: "ig-post-12345"
|
||||
- channel: "NAVER_BLOG"
|
||||
completedAt: "2025-11-01T09:00:00"
|
||||
- channel: "NAVER"
|
||||
status: "COMPLETED"
|
||||
postUrl: "https://blog.naver.com/store123/generated-post"
|
||||
- channel: "KAKAO_CHANNEL"
|
||||
completedAt: "2025-11-01T09:00:00"
|
||||
- channel: "KAKAO"
|
||||
status: "COMPLETED"
|
||||
messageId: "kakao-msg-12345"
|
||||
completedAt: "2025-11-01T09:00:00"
|
||||
inProgress:
|
||||
summary: 배포 진행중 상태
|
||||
value:
|
||||
eventId: "evt-12345"
|
||||
overallStatus: "IN_PROGRESS"
|
||||
startedAt: "2025-11-01T08:58:00Z"
|
||||
startedAt: "2025-11-01T08:58:00"
|
||||
channels:
|
||||
- channel: "WOORIDONGNE_TV"
|
||||
- channel: "URIDONGNETV"
|
||||
status: "COMPLETED"
|
||||
distributionId: "wtv-uuid-12345"
|
||||
estimatedViews: 1500
|
||||
completedAt: "2025-11-01T08:59:00"
|
||||
- channel: "INSTAGRAM"
|
||||
status: "IN_PROGRESS"
|
||||
progress: 50
|
||||
- channel: "NAVER_BLOG"
|
||||
- channel: "NAVER"
|
||||
status: "PENDING"
|
||||
partialFailure:
|
||||
summary: 일부 채널 실패 상태
|
||||
value:
|
||||
eventId: "evt-12345"
|
||||
overallStatus: "PARTIAL_FAILURE"
|
||||
completedAt: "2025-11-01T09:00:00Z"
|
||||
startedAt: "2025-11-01T08:58:00"
|
||||
completedAt: "2025-11-01T09:00:00"
|
||||
channels:
|
||||
- channel: "WOORIDONGNE_TV"
|
||||
- channel: "URIDONGNETV"
|
||||
status: "COMPLETED"
|
||||
distributionId: "wtv-uuid-12345"
|
||||
estimatedViews: 1500
|
||||
completedAt: "2025-11-01T08:59:00"
|
||||
- channel: "INSTAGRAM"
|
||||
status: "FAILED"
|
||||
errorMessage: "Instagram API 타임아웃"
|
||||
retries: 3
|
||||
lastRetryAt: "2025-11-01T08:59:30Z"
|
||||
- channel: "NAVER_BLOG"
|
||||
lastRetryAt: "2025-11-01T08:59:30"
|
||||
- channel: "NAVER"
|
||||
status: "COMPLETED"
|
||||
postUrl: "https://blog.naver.com/store123/generated-post"
|
||||
completedAt: "2025-11-01T09:00:00"
|
||||
'404':
|
||||
description: 배포 이력을 찾을 수 없음
|
||||
content:
|
||||
@ -305,196 +317,133 @@ components:
|
||||
required:
|
||||
- eventId
|
||||
- channels
|
||||
- contentUrls
|
||||
properties:
|
||||
eventId:
|
||||
type: string
|
||||
description: 이벤트 ID
|
||||
example: "evt-12345"
|
||||
title:
|
||||
type: string
|
||||
description: 이벤트 제목
|
||||
example: "신규 고객 환영 이벤트"
|
||||
description:
|
||||
type: string
|
||||
description: 이벤트 설명
|
||||
example: "신규 고객님을 위한 특별 할인 이벤트"
|
||||
imageUrl:
|
||||
type: string
|
||||
description: 이미지 URL (CDN)
|
||||
example: "https://cdn.example.com/images/event-main.jpg"
|
||||
channels:
|
||||
type: array
|
||||
description: 배포할 채널 목록
|
||||
minItems: 1
|
||||
items:
|
||||
$ref: '#/components/schemas/ChannelConfig'
|
||||
contentUrls:
|
||||
type: string
|
||||
enum:
|
||||
- URIDONGNETV
|
||||
- RINGOBIZ
|
||||
- GINITV
|
||||
- INSTAGRAM
|
||||
- NAVER
|
||||
- KAKAO
|
||||
example: ["URIDONGNETV", "INSTAGRAM", "NAVER"]
|
||||
channelSettings:
|
||||
type: object
|
||||
description: 플랫폼별 콘텐츠 URL
|
||||
properties:
|
||||
wooridongneTV:
|
||||
type: string
|
||||
description: 우리동네TV 영상 URL (15초)
|
||||
example: "https://cdn.example.com/videos/event-15s.mp4"
|
||||
ringoBiz:
|
||||
type: string
|
||||
description: 링고비즈 연결음 파일 URL
|
||||
example: "https://cdn.example.com/audio/ringtone.mp3"
|
||||
genieTV:
|
||||
type: string
|
||||
description: 지니TV 광고 영상 URL
|
||||
example: "https://cdn.example.com/videos/event-ad.mp4"
|
||||
instagram:
|
||||
type: string
|
||||
description: Instagram 이미지 URL (1080x1080)
|
||||
example: "https://cdn.example.com/images/event-instagram.jpg"
|
||||
naverBlog:
|
||||
type: string
|
||||
description: Naver Blog 이미지 URL (800x600)
|
||||
example: "https://cdn.example.com/images/event-naver.jpg"
|
||||
kakaoChannel:
|
||||
type: string
|
||||
description: Kakao Channel 이미지 URL (800x800)
|
||||
example: "https://cdn.example.com/images/event-kakao.jpg"
|
||||
|
||||
ChannelConfig:
|
||||
type: object
|
||||
required:
|
||||
- type
|
||||
properties:
|
||||
type:
|
||||
type: string
|
||||
description: 채널 타입
|
||||
enum:
|
||||
- WOORIDONGNE_TV
|
||||
- RINGO_BIZ
|
||||
- GENIE_TV
|
||||
- INSTAGRAM
|
||||
- NAVER_BLOG
|
||||
- KAKAO_CHANNEL
|
||||
example: "INSTAGRAM"
|
||||
config:
|
||||
type: object
|
||||
description: 채널별 설정 (채널에 따라 다름)
|
||||
additionalProperties: true
|
||||
description: 채널별 추가 설정 (Optional)
|
||||
additionalProperties:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
example:
|
||||
scheduledTime: "2025-11-01T10:00:00Z"
|
||||
caption: "이벤트 안내"
|
||||
hashtags:
|
||||
- "이벤트"
|
||||
- "할인"
|
||||
URIDONGNETV:
|
||||
radius: "1km"
|
||||
timeSlot: "evening"
|
||||
INSTAGRAM:
|
||||
scheduledTime: "2025-11-01T10:00:00"
|
||||
|
||||
DistributionResponse:
|
||||
type: object
|
||||
required:
|
||||
- distributionId
|
||||
- eventId
|
||||
- status
|
||||
- results
|
||||
- success
|
||||
- channelResults
|
||||
- successCount
|
||||
- failureCount
|
||||
properties:
|
||||
distributionId:
|
||||
type: string
|
||||
description: 배포 ID
|
||||
example: "dist-12345"
|
||||
eventId:
|
||||
type: string
|
||||
description: 이벤트 ID
|
||||
example: "evt-12345"
|
||||
status:
|
||||
type: string
|
||||
description: 전체 배포 상태
|
||||
enum:
|
||||
- PENDING
|
||||
- IN_PROGRESS
|
||||
- COMPLETED
|
||||
- PARTIAL_FAILURE
|
||||
- FAILED
|
||||
example: "COMPLETED"
|
||||
startedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 배포 시작 시각
|
||||
example: "2025-11-01T08:59:00Z"
|
||||
success:
|
||||
type: boolean
|
||||
description: 배포 성공 여부 (모든 채널 또는 일부 채널 성공)
|
||||
example: true
|
||||
channelResults:
|
||||
type: array
|
||||
description: 채널별 배포 결과
|
||||
items:
|
||||
$ref: '#/components/schemas/ChannelDistributionResult'
|
||||
successCount:
|
||||
type: integer
|
||||
description: 성공한 채널 수
|
||||
example: 3
|
||||
failureCount:
|
||||
type: integer
|
||||
description: 실패한 채널 수
|
||||
example: 0
|
||||
completedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 배포 완료 시각
|
||||
example: "2025-11-01T09:00:00Z"
|
||||
results:
|
||||
type: array
|
||||
description: 채널별 배포 결과
|
||||
items:
|
||||
$ref: '#/components/schemas/ChannelResult'
|
||||
example: "2025-11-01T09:00:00"
|
||||
totalExecutionTimeMs:
|
||||
type: integer
|
||||
format: int64
|
||||
description: 전체 배포 소요 시간 (ms)
|
||||
example: 1234
|
||||
message:
|
||||
type: string
|
||||
description: 메시지
|
||||
example: "배포가 성공적으로 완료되었습니다"
|
||||
|
||||
ChannelResult:
|
||||
ChannelDistributionResult:
|
||||
type: object
|
||||
required:
|
||||
- channel
|
||||
- status
|
||||
- success
|
||||
properties:
|
||||
channel:
|
||||
type: string
|
||||
description: 채널 타입
|
||||
enum:
|
||||
- WOORIDONGNE_TV
|
||||
- RINGO_BIZ
|
||||
- GENIE_TV
|
||||
- URIDONGNETV
|
||||
- RINGOBIZ
|
||||
- GINITV
|
||||
- INSTAGRAM
|
||||
- NAVER_BLOG
|
||||
- KAKAO_CHANNEL
|
||||
- NAVER
|
||||
- KAKAO
|
||||
example: "INSTAGRAM"
|
||||
status:
|
||||
type: string
|
||||
description: 채널별 배포 상태
|
||||
enum:
|
||||
- PENDING
|
||||
- IN_PROGRESS
|
||||
- SUCCESS
|
||||
- FAILED
|
||||
example: "SUCCESS"
|
||||
success:
|
||||
type: boolean
|
||||
description: 배포 성공 여부
|
||||
example: true
|
||||
distributionId:
|
||||
type: string
|
||||
description: 채널별 배포 ID (우리동네TV, 지니TV)
|
||||
example: "wtv-uuid-12345"
|
||||
estimatedViews:
|
||||
description: 배포 ID (성공 시)
|
||||
example: "dist-uuid-12345"
|
||||
estimatedReach:
|
||||
type: integer
|
||||
description: 예상 노출 수 (우리동네TV, 지니TV)
|
||||
description: 예상 노출 수 (성공 시)
|
||||
example: 1500
|
||||
updateTimestamp:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 업데이트 완료 시각 (링고비즈)
|
||||
example: "2025-11-01T09:00:00Z"
|
||||
adId:
|
||||
type: string
|
||||
description: 광고 ID (지니TV)
|
||||
example: "gtv-uuid-12345"
|
||||
impressionSchedule:
|
||||
type: array
|
||||
description: 노출 스케줄 (지니TV)
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
- "2025-11-01 18:00-20:00"
|
||||
- "2025-11-02 12:00-14:00"
|
||||
postUrl:
|
||||
type: string
|
||||
description: 게시물 URL (Instagram, Naver Blog)
|
||||
example: "https://instagram.com/p/generated-post-id"
|
||||
postId:
|
||||
type: string
|
||||
description: 게시물 ID (Instagram)
|
||||
example: "ig-post-12345"
|
||||
messageId:
|
||||
type: string
|
||||
description: 메시지 ID (Kakao Channel)
|
||||
example: "kakao-msg-12345"
|
||||
message:
|
||||
type: string
|
||||
description: 결과 메시지
|
||||
example: "배포 완료"
|
||||
errorMessage:
|
||||
type: string
|
||||
description: 오류 메시지 (실패 시)
|
||||
description: 에러 메시지 (실패 시)
|
||||
example: "Instagram API 타임아웃"
|
||||
retries:
|
||||
executionTimeMs:
|
||||
type: integer
|
||||
description: 재시도 횟수
|
||||
example: 0
|
||||
lastRetryAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 마지막 재시도 시각
|
||||
example: "2025-11-01T08:59:30Z"
|
||||
format: int64
|
||||
description: 배포 소요 시간 (ms)
|
||||
example: 234
|
||||
|
||||
DistributionStatusResponse:
|
||||
type: object
|
||||
@ -544,12 +493,12 @@ components:
|
||||
type: string
|
||||
description: 채널 타입
|
||||
enum:
|
||||
- WOORIDONGNE_TV
|
||||
- RINGO_BIZ
|
||||
- GENIE_TV
|
||||
- URIDONGNETV
|
||||
- RINGOBIZ
|
||||
- GINITV
|
||||
- INSTAGRAM
|
||||
- NAVER_BLOG
|
||||
- KAKAO_CHANNEL
|
||||
- NAVER
|
||||
- KAKAO
|
||||
example: "INSTAGRAM"
|
||||
status:
|
||||
type: string
|
||||
@ -569,7 +518,7 @@ components:
|
||||
distributionId:
|
||||
type: string
|
||||
description: 채널별 배포 ID
|
||||
example: "wtv-uuid-12345"
|
||||
example: "dist-uuid-12345"
|
||||
estimatedViews:
|
||||
type: integer
|
||||
description: 예상 노출 수
|
||||
@ -578,35 +527,35 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 업데이트 완료 시각
|
||||
example: "2025-11-01T09:00:00Z"
|
||||
example: "2025-11-01T09:00:00"
|
||||
adId:
|
||||
type: string
|
||||
description: 광고 ID
|
||||
description: 광고 ID (지니TV)
|
||||
example: "gtv-uuid-12345"
|
||||
impressionSchedule:
|
||||
type: array
|
||||
description: 노출 스케줄
|
||||
description: 노출 스케줄 (지니TV)
|
||||
items:
|
||||
type: string
|
||||
example:
|
||||
- "2025-11-01 18:00-20:00"
|
||||
postUrl:
|
||||
type: string
|
||||
description: 게시물 URL
|
||||
description: 게시물 URL (Instagram, Naver Blog)
|
||||
example: "https://instagram.com/p/generated-post-id"
|
||||
postId:
|
||||
type: string
|
||||
description: 게시물 ID
|
||||
description: 게시물 ID (Instagram)
|
||||
example: "ig-post-12345"
|
||||
messageId:
|
||||
type: string
|
||||
description: 메시지 ID
|
||||
description: 메시지 ID (Kakao Channel)
|
||||
example: "kakao-msg-12345"
|
||||
completedAt:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 완료 시각
|
||||
example: "2025-11-01T09:00:00Z"
|
||||
example: "2025-11-01T09:00:00"
|
||||
errorMessage:
|
||||
type: string
|
||||
description: 오류 메시지
|
||||
@ -619,7 +568,7 @@ components:
|
||||
type: string
|
||||
format: date-time
|
||||
description: 마지막 재시도 시각
|
||||
example: "2025-11-01T08:59:30Z"
|
||||
example: "2025-11-01T08:59:30"
|
||||
|
||||
ErrorResponse:
|
||||
type: object
|
||||
|
||||
@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.*;
|
||||
*/
|
||||
@Slf4j
|
||||
@RestController
|
||||
@RequestMapping("/api/distribution")
|
||||
@RequestMapping("/distribution")
|
||||
@RequiredArgsConstructor
|
||||
public class DistributionController {
|
||||
|
||||
|
||||
@ -0,0 +1,168 @@
|
||||
package com.kt.distribution.entity;
|
||||
|
||||
import com.kt.distribution.dto.ChannelType;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 채널별 배포 상태 엔티티
|
||||
*
|
||||
* 각 채널의 배포 진행 상태 및 결과 정보를 저장합니다.
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 2025-10-24
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "channel_status", indexes = {
|
||||
@Index(name = "idx_distribution_channel", columnList = "distribution_status_id, channel")
|
||||
})
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ChannelStatusEntity {
|
||||
|
||||
/**
|
||||
* 채널 상태 ID (Primary Key)
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 배포 상태 (Foreign Key)
|
||||
*/
|
||||
@ManyToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "distribution_status_id", nullable = false)
|
||||
private DistributionStatus distributionStatus;
|
||||
|
||||
/**
|
||||
* 채널 타입
|
||||
*/
|
||||
@Enumerated(EnumType.STRING)
|
||||
@Column(name = "channel", nullable = false, length = 20)
|
||||
private ChannelType channel;
|
||||
|
||||
/**
|
||||
* 채널별 배포 상태
|
||||
* - PENDING: 대기 중
|
||||
* - IN_PROGRESS: 진행 중
|
||||
* - COMPLETED: 완료
|
||||
* - FAILED: 실패
|
||||
*/
|
||||
@Column(name = "status", nullable = false, length = 20)
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 진행률 (0-100, IN_PROGRESS 상태일 때 사용)
|
||||
*/
|
||||
@Column(name = "progress")
|
||||
private Integer progress;
|
||||
|
||||
/**
|
||||
* 채널별 배포 ID (우리동네TV, 지니TV 등)
|
||||
*/
|
||||
@Column(name = "distribution_id", length = 100)
|
||||
private String distributionId;
|
||||
|
||||
/**
|
||||
* 예상 노출 수 (우리동네TV, 지니TV)
|
||||
*/
|
||||
@Column(name = "estimated_views")
|
||||
private Integer estimatedViews;
|
||||
|
||||
/**
|
||||
* 업데이트 완료 시각 (링고비즈)
|
||||
*/
|
||||
@Column(name = "update_timestamp")
|
||||
private LocalDateTime updateTimestamp;
|
||||
|
||||
/**
|
||||
* 광고 ID (지니TV)
|
||||
*/
|
||||
@Column(name = "ad_id", length = 100)
|
||||
private String adId;
|
||||
|
||||
/**
|
||||
* 노출 스케줄 (지니TV) - JSON 형태로 저장
|
||||
*/
|
||||
@Column(name = "impression_schedule", columnDefinition = "TEXT")
|
||||
private String impressionSchedule;
|
||||
|
||||
/**
|
||||
* 게시물 URL (Instagram, Naver Blog)
|
||||
*/
|
||||
@Column(name = "post_url", columnDefinition = "TEXT")
|
||||
private String postUrl;
|
||||
|
||||
/**
|
||||
* 게시물 ID (Instagram)
|
||||
*/
|
||||
@Column(name = "post_id", length = 100)
|
||||
private String postId;
|
||||
|
||||
/**
|
||||
* 메시지 ID (Kakao Channel)
|
||||
*/
|
||||
@Column(name = "message_id", length = 100)
|
||||
private String messageId;
|
||||
|
||||
/**
|
||||
* 완료 시각
|
||||
*/
|
||||
@Column(name = "completed_at")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
/**
|
||||
* 오류 메시지 (실패 시)
|
||||
*/
|
||||
@Column(name = "error_message", columnDefinition = "TEXT")
|
||||
private String errorMessage;
|
||||
|
||||
/**
|
||||
* 재시도 횟수
|
||||
*/
|
||||
@Column(name = "retries")
|
||||
@Builder.Default
|
||||
private Integer retries = 0;
|
||||
|
||||
/**
|
||||
* 마지막 재시도 시각
|
||||
*/
|
||||
@Column(name = "last_retry_at")
|
||||
private LocalDateTime lastRetryAt;
|
||||
|
||||
/**
|
||||
* 생성 시각
|
||||
*/
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 수정 시각
|
||||
*/
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 생성 시 자동으로 생성 시각 설정
|
||||
*/
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정 시 자동으로 수정 시각 설정
|
||||
*/
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,118 @@
|
||||
package com.kt.distribution.entity;
|
||||
|
||||
import jakarta.persistence.*;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 배포 상태 엔티티
|
||||
*
|
||||
* 이벤트의 전체 배포 상태 정보를 저장합니다.
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 2025-10-24
|
||||
*/
|
||||
@Entity
|
||||
@Table(name = "distribution_status", indexes = {
|
||||
@Index(name = "idx_event_id", columnList = "event_id", unique = true)
|
||||
})
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class DistributionStatus {
|
||||
|
||||
/**
|
||||
* 배포 상태 ID (Primary Key)
|
||||
*/
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
/**
|
||||
* 이벤트 ID (Unique)
|
||||
*/
|
||||
@Column(name = "event_id", nullable = false, unique = true, length = 100)
|
||||
private String eventId;
|
||||
|
||||
/**
|
||||
* 전체 배포 상태
|
||||
* - PENDING: 대기 중
|
||||
* - IN_PROGRESS: 진행 중
|
||||
* - COMPLETED: 완료
|
||||
* - PARTIAL_FAILURE: 부분 성공
|
||||
* - FAILED: 실패
|
||||
*/
|
||||
@Column(name = "overall_status", nullable = false, length = 20)
|
||||
private String overallStatus;
|
||||
|
||||
/**
|
||||
* 배포 시작 시각
|
||||
*/
|
||||
@Column(name = "started_at")
|
||||
private LocalDateTime startedAt;
|
||||
|
||||
/**
|
||||
* 배포 완료 시각
|
||||
*/
|
||||
@Column(name = "completed_at")
|
||||
private LocalDateTime completedAt;
|
||||
|
||||
/**
|
||||
* 채널별 배포 상태 목록 (1:N 관계)
|
||||
*/
|
||||
@OneToMany(mappedBy = "distributionStatus", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
|
||||
@Builder.Default
|
||||
private List<ChannelStatusEntity> channels = new ArrayList<>();
|
||||
|
||||
/**
|
||||
* 생성 시각
|
||||
*/
|
||||
@Column(name = "created_at", nullable = false, updatable = false)
|
||||
private LocalDateTime createdAt;
|
||||
|
||||
/**
|
||||
* 수정 시각
|
||||
*/
|
||||
@Column(name = "updated_at")
|
||||
private LocalDateTime updatedAt;
|
||||
|
||||
/**
|
||||
* 생성 시 자동으로 생성 시각 설정
|
||||
*/
|
||||
@PrePersist
|
||||
protected void onCreate() {
|
||||
createdAt = LocalDateTime.now();
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 수정 시 자동으로 수정 시각 설정
|
||||
*/
|
||||
@PreUpdate
|
||||
protected void onUpdate() {
|
||||
updatedAt = LocalDateTime.now();
|
||||
}
|
||||
|
||||
/**
|
||||
* 채널 상태 추가 헬퍼 메서드
|
||||
*/
|
||||
public void addChannelStatus(ChannelStatusEntity channelStatus) {
|
||||
channels.add(channelStatus);
|
||||
channelStatus.setDistributionStatus(this);
|
||||
}
|
||||
|
||||
/**
|
||||
* 채널 상태 제거 헬퍼 메서드
|
||||
*/
|
||||
public void removeChannelStatus(ChannelStatusEntity channelStatus) {
|
||||
channels.remove(channelStatus);
|
||||
channelStatus.setDistributionStatus(null);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
package com.kt.distribution.mapper;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.core.type.TypeReference;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.kt.distribution.dto.ChannelStatus;
|
||||
import com.kt.distribution.dto.DistributionStatusResponse;
|
||||
import com.kt.distribution.entity.ChannelStatusEntity;
|
||||
import com.kt.distribution.entity.DistributionStatus;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
/**
|
||||
* 배포 상태 Mapper
|
||||
*
|
||||
* Entity와 DTO 간의 변환을 담당합니다.
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 2025-10-24
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class DistributionStatusMapper {
|
||||
|
||||
private final ObjectMapper objectMapper;
|
||||
|
||||
/**
|
||||
* DistributionStatusResponse DTO를 DistributionStatus Entity로 변환
|
||||
*
|
||||
* @param dto DistributionStatusResponse DTO
|
||||
* @return DistributionStatus Entity
|
||||
*/
|
||||
public DistributionStatus toEntity(DistributionStatusResponse dto) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
DistributionStatus entity = DistributionStatus.builder()
|
||||
.eventId(dto.getEventId())
|
||||
.overallStatus(dto.getOverallStatus())
|
||||
.startedAt(dto.getStartedAt())
|
||||
.completedAt(dto.getCompletedAt())
|
||||
.build();
|
||||
|
||||
// 채널 상태 변환 및 추가
|
||||
if (dto.getChannels() != null) {
|
||||
List<ChannelStatusEntity> channelEntities = dto.getChannels().stream()
|
||||
.map(channelDto -> toChannelEntity(channelDto, entity))
|
||||
.collect(Collectors.toList());
|
||||
|
||||
channelEntities.forEach(entity::addChannelStatus);
|
||||
}
|
||||
|
||||
return entity;
|
||||
}
|
||||
|
||||
/**
|
||||
* DistributionStatus Entity를 DistributionStatusResponse DTO로 변환
|
||||
*
|
||||
* @param entity DistributionStatus Entity
|
||||
* @return DistributionStatusResponse DTO
|
||||
*/
|
||||
public DistributionStatusResponse toDto(DistributionStatus entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
List<ChannelStatus> channelDtos = entity.getChannels() != null
|
||||
? entity.getChannels().stream()
|
||||
.map(this::toChannelDto)
|
||||
.collect(Collectors.toList())
|
||||
: Collections.emptyList();
|
||||
|
||||
return DistributionStatusResponse.builder()
|
||||
.eventId(entity.getEventId())
|
||||
.overallStatus(entity.getOverallStatus())
|
||||
.startedAt(entity.getStartedAt())
|
||||
.completedAt(entity.getCompletedAt())
|
||||
.channels(channelDtos)
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* ChannelStatus DTO를 ChannelStatusEntity로 변환
|
||||
*
|
||||
* @param dto ChannelStatus DTO
|
||||
* @param distributionStatus 부모 DistributionStatus Entity
|
||||
* @return ChannelStatusEntity
|
||||
*/
|
||||
private ChannelStatusEntity toChannelEntity(ChannelStatus dto, DistributionStatus distributionStatus) {
|
||||
if (dto == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// impressionSchedule를 JSON 문자열로 변환
|
||||
String impressionScheduleJson = null;
|
||||
if (dto.getImpressionSchedule() != null && !dto.getImpressionSchedule().isEmpty()) {
|
||||
try {
|
||||
impressionScheduleJson = objectMapper.writeValueAsString(dto.getImpressionSchedule());
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to serialize impressionSchedule", e);
|
||||
}
|
||||
}
|
||||
|
||||
return ChannelStatusEntity.builder()
|
||||
.distributionStatus(distributionStatus)
|
||||
.channel(dto.getChannel())
|
||||
.status(dto.getStatus())
|
||||
.progress(dto.getProgress())
|
||||
.distributionId(dto.getDistributionId())
|
||||
.estimatedViews(dto.getEstimatedViews())
|
||||
.updateTimestamp(dto.getUpdateTimestamp())
|
||||
.adId(dto.getAdId())
|
||||
.impressionSchedule(impressionScheduleJson)
|
||||
.postUrl(dto.getPostUrl())
|
||||
.postId(dto.getPostId())
|
||||
.messageId(dto.getMessageId())
|
||||
.completedAt(dto.getCompletedAt())
|
||||
.errorMessage(dto.getErrorMessage())
|
||||
.retries(dto.getRetries())
|
||||
.lastRetryAt(dto.getLastRetryAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* ChannelStatusEntity를 ChannelStatus DTO로 변환
|
||||
*
|
||||
* @param entity ChannelStatusEntity
|
||||
* @return ChannelStatus DTO
|
||||
*/
|
||||
private ChannelStatus toChannelDto(ChannelStatusEntity entity) {
|
||||
if (entity == null) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// JSON 문자열을 List<String>으로 변환
|
||||
List<String> impressionScheduleList = null;
|
||||
if (entity.getImpressionSchedule() != null && !entity.getImpressionSchedule().isEmpty()) {
|
||||
try {
|
||||
impressionScheduleList = objectMapper.readValue(
|
||||
entity.getImpressionSchedule(),
|
||||
new TypeReference<List<String>>() {}
|
||||
);
|
||||
} catch (JsonProcessingException e) {
|
||||
log.error("Failed to deserialize impressionSchedule", e);
|
||||
}
|
||||
}
|
||||
|
||||
return ChannelStatus.builder()
|
||||
.channel(entity.getChannel())
|
||||
.status(entity.getStatus())
|
||||
.progress(entity.getProgress())
|
||||
.distributionId(entity.getDistributionId())
|
||||
.estimatedViews(entity.getEstimatedViews())
|
||||
.updateTimestamp(entity.getUpdateTimestamp())
|
||||
.adId(entity.getAdId())
|
||||
.impressionSchedule(impressionScheduleList)
|
||||
.postUrl(entity.getPostUrl())
|
||||
.postId(entity.getPostId())
|
||||
.messageId(entity.getMessageId())
|
||||
.completedAt(entity.getCompletedAt())
|
||||
.errorMessage(entity.getErrorMessage())
|
||||
.retries(entity.getRetries())
|
||||
.lastRetryAt(entity.getLastRetryAt())
|
||||
.build();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,53 @@
|
||||
package com.kt.distribution.repository;
|
||||
|
||||
import com.kt.distribution.entity.DistributionStatus;
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
import org.springframework.data.jpa.repository.Query;
|
||||
import org.springframework.data.repository.query.Param;
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
/**
|
||||
* 배포 상태 JPA Repository
|
||||
*
|
||||
* 배포 상태를 데이터베이스에 영구 저장하고 조회합니다.
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 2025-10-24
|
||||
*/
|
||||
@Repository
|
||||
public interface DistributionStatusJpaRepository extends JpaRepository<DistributionStatus, Long> {
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 배포 상태 조회
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 배포 상태 (없으면 Optional.empty())
|
||||
*/
|
||||
Optional<DistributionStatus> findByEventId(String eventId);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 배포 상태 조회 (채널 상태 Fetch Join)
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 배포 상태 (채널 상태 포함, 없으면 Optional.empty())
|
||||
*/
|
||||
@Query("SELECT d FROM DistributionStatus d LEFT JOIN FETCH d.channels WHERE d.eventId = :eventId")
|
||||
Optional<DistributionStatus> findByEventIdWithChannels(@Param("eventId") String eventId);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 배포 상태 존재 여부 확인
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 존재 여부
|
||||
*/
|
||||
boolean existsByEventId(String eventId);
|
||||
|
||||
/**
|
||||
* 이벤트 ID로 배포 상태 삭제
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
*/
|
||||
void deleteByEventId(String eventId);
|
||||
}
|
||||
@ -1,48 +1,78 @@
|
||||
package com.kt.distribution.repository;
|
||||
|
||||
import com.kt.distribution.dto.DistributionStatusResponse;
|
||||
import com.kt.distribution.entity.DistributionStatus;
|
||||
import com.kt.distribution.mapper.DistributionStatusMapper;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Repository;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.Optional;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 배포 상태 저장소
|
||||
*
|
||||
* 메모리 기반으로 배포 상태를 관리합니다.
|
||||
* 실제 운영 환경에서는 Redis 또는 데이터베이스를 사용하여 영구 저장하는 것을 권장합니다.
|
||||
* PostgreSQL 데이터베이스를 사용하여 배포 상태를 영구 저장합니다.
|
||||
*
|
||||
* @author Backend Developer
|
||||
* @since 2025-10-24
|
||||
*/
|
||||
@Slf4j
|
||||
@Repository
|
||||
@RequiredArgsConstructor
|
||||
public class DistributionStatusRepository {
|
||||
|
||||
/**
|
||||
* 이벤트 ID를 키로 배포 상태를 저장하는 메모리 저장소
|
||||
*/
|
||||
private final Map<String, DistributionStatusResponse> distributionStatuses = new ConcurrentHashMap<>();
|
||||
private final DistributionStatusJpaRepository jpaRepository;
|
||||
private final DistributionStatusMapper mapper;
|
||||
|
||||
/**
|
||||
* 배포 상태 저장
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @param status 배포 상태
|
||||
* @param status 배포 상태 DTO
|
||||
*/
|
||||
@Transactional
|
||||
public void save(String eventId, DistributionStatusResponse status) {
|
||||
log.debug("Saving distribution status: eventId={}, overallStatus={}", eventId, status.getOverallStatus());
|
||||
distributionStatuses.put(eventId, status);
|
||||
|
||||
// 기존 데이터가 있으면 업데이트, 없으면 새로 생성
|
||||
Optional<DistributionStatus> existingStatus = jpaRepository.findByEventIdWithChannels(eventId);
|
||||
|
||||
if (existingStatus.isPresent()) {
|
||||
// 기존 데이터 업데이트
|
||||
DistributionStatus entity = existingStatus.get();
|
||||
entity.setOverallStatus(status.getOverallStatus());
|
||||
entity.setStartedAt(status.getStartedAt());
|
||||
entity.setCompletedAt(status.getCompletedAt());
|
||||
|
||||
// 기존 채널 상태 모두 삭제 후 새로 추가
|
||||
entity.getChannels().clear();
|
||||
|
||||
DistributionStatus newEntity = mapper.toEntity(status);
|
||||
if (newEntity.getChannels() != null) {
|
||||
newEntity.getChannels().forEach(entity::addChannelStatus);
|
||||
}
|
||||
|
||||
jpaRepository.save(entity);
|
||||
} else {
|
||||
// 새로 생성
|
||||
DistributionStatus entity = mapper.toEntity(status);
|
||||
jpaRepository.save(entity);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 배포 상태 조회
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
* @return 배포 상태 (없으면 Optional.empty())
|
||||
* @return 배포 상태 DTO (없으면 Optional.empty())
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
public Optional<DistributionStatusResponse> findByEventId(String eventId) {
|
||||
log.debug("Finding distribution status: eventId={}", eventId);
|
||||
return Optional.ofNullable(distributionStatuses.get(eventId));
|
||||
return jpaRepository.findByEventIdWithChannels(eventId)
|
||||
.map(mapper::toDto);
|
||||
}
|
||||
|
||||
/**
|
||||
@ -50,16 +80,18 @@ public class DistributionStatusRepository {
|
||||
*
|
||||
* @param eventId 이벤트 ID
|
||||
*/
|
||||
@Transactional
|
||||
public void delete(String eventId) {
|
||||
log.debug("Deleting distribution status: eventId={}", eventId);
|
||||
distributionStatuses.remove(eventId);
|
||||
jpaRepository.deleteByEventId(eventId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 모든 배포 상태 삭제 (테스트용)
|
||||
*/
|
||||
@Transactional
|
||||
public void deleteAll() {
|
||||
log.debug("Deleting all distribution statuses");
|
||||
distributionStatuses.clear();
|
||||
jpaRepository.deleteAll();
|
||||
}
|
||||
}
|
||||
|
||||
120
tools/check_tables.py
Normal file
120
tools/check_tables.py
Normal file
@ -0,0 +1,120 @@
|
||||
#!/usr/bin/env python3
|
||||
"""
|
||||
PostgreSQL 테이블 확인 스크립트
|
||||
distribution-service의 테이블 생성 여부를 확인합니다.
|
||||
"""
|
||||
|
||||
import psycopg2
|
||||
import sys
|
||||
|
||||
# DB 연결 정보
|
||||
DB_CONFIG = {
|
||||
'host': '4.217.133.59',
|
||||
'port': 5432,
|
||||
'database': 'distributiondb',
|
||||
'user': 'eventuser',
|
||||
'password': 'Hi5Jessica!'
|
||||
}
|
||||
|
||||
def main():
|
||||
try:
|
||||
# DB 연결
|
||||
print(f"Connecting to database: {DB_CONFIG['host']}:{DB_CONFIG['port']}/{DB_CONFIG['database']}")
|
||||
conn = psycopg2.connect(**DB_CONFIG)
|
||||
cursor = conn.cursor()
|
||||
|
||||
# 테이블 목록 조회
|
||||
print("\n=== Tables in distributiondb ===")
|
||||
cursor.execute("""
|
||||
SELECT table_name
|
||||
FROM information_schema.tables
|
||||
WHERE table_schema = 'public'
|
||||
ORDER BY table_name
|
||||
""")
|
||||
tables = cursor.fetchall()
|
||||
|
||||
if not tables:
|
||||
print("No tables found.")
|
||||
else:
|
||||
for table in tables:
|
||||
print(f" - {table[0]}")
|
||||
|
||||
# distribution_status 테이블 구조 확인
|
||||
if any('distribution_status' in table for table in tables):
|
||||
print("\n=== distribution_status table structure ===")
|
||||
cursor.execute("""
|
||||
SELECT column_name, data_type, character_maximum_length, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'distribution_status'
|
||||
ORDER BY ordinal_position
|
||||
""")
|
||||
columns = cursor.fetchall()
|
||||
for col in columns:
|
||||
nullable = "NULL" if col[3] == 'YES' else "NOT NULL"
|
||||
max_len = f"({col[2]})" if col[2] else ""
|
||||
print(f" - {col[0]}: {col[1]}{max_len} {nullable}")
|
||||
|
||||
# channel_status 테이블 구조 확인
|
||||
if any('channel_status' in table for table in tables):
|
||||
print("\n=== channel_status table structure ===")
|
||||
cursor.execute("""
|
||||
SELECT column_name, data_type, character_maximum_length, is_nullable
|
||||
FROM information_schema.columns
|
||||
WHERE table_name = 'channel_status'
|
||||
ORDER BY ordinal_position
|
||||
""")
|
||||
columns = cursor.fetchall()
|
||||
for col in columns:
|
||||
nullable = "NULL" if col[3] == 'YES' else "NOT NULL"
|
||||
max_len = f"({col[2]})" if col[2] else ""
|
||||
print(f" - {col[0]}: {col[1]}{max_len} {nullable}")
|
||||
|
||||
# 인덱스 확인
|
||||
print("\n=== Indexes ===")
|
||||
cursor.execute("""
|
||||
SELECT tablename, indexname, indexdef
|
||||
FROM pg_indexes
|
||||
WHERE schemaname = 'public'
|
||||
AND tablename IN ('distribution_status', 'channel_status')
|
||||
ORDER BY tablename, indexname
|
||||
""")
|
||||
indexes = cursor.fetchall()
|
||||
for idx in indexes:
|
||||
print(f" - {idx[0]}.{idx[1]}")
|
||||
print(f" {idx[2]}")
|
||||
|
||||
# 외래 키 확인
|
||||
print("\n=== Foreign Keys ===")
|
||||
cursor.execute("""
|
||||
SELECT
|
||||
tc.table_name,
|
||||
kcu.column_name,
|
||||
ccu.table_name AS foreign_table_name,
|
||||
ccu.column_name AS foreign_column_name
|
||||
FROM information_schema.table_constraints AS tc
|
||||
JOIN information_schema.key_column_usage AS kcu
|
||||
ON tc.constraint_name = kcu.constraint_name
|
||||
AND tc.table_schema = kcu.table_schema
|
||||
JOIN information_schema.constraint_column_usage AS ccu
|
||||
ON ccu.constraint_name = tc.constraint_name
|
||||
AND ccu.table_schema = tc.table_schema
|
||||
WHERE tc.constraint_type = 'FOREIGN KEY'
|
||||
AND tc.table_name IN ('distribution_status', 'channel_status')
|
||||
""")
|
||||
fks = cursor.fetchall()
|
||||
for fk in fks:
|
||||
print(f" - {fk[0]}.{fk[1]} -> {fk[2]}.{fk[3]}")
|
||||
|
||||
# 연결 종료
|
||||
cursor.close()
|
||||
conn.close()
|
||||
|
||||
print("\n✅ Database connection successful!")
|
||||
return 0
|
||||
|
||||
except Exception as e:
|
||||
print(f"\n❌ Error: {e}", file=sys.stderr)
|
||||
return 1
|
||||
|
||||
if __name__ == "__main__":
|
||||
sys.exit(main())
|
||||
Loading…
x
Reference in New Issue
Block a user