mirror of
https://github.com/ktds-dg0501/kt-event-marketing.git
synced 2025-12-06 14:06:23 +00:00
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 처리
|
- Retry 패턴 및 Fallback 처리
|
||||||
|
|
||||||
## 배포 채널
|
## 배포 채널
|
||||||
- **우리동네TV**: 영상 콘텐츠 업로드
|
- **우리동네TV** (URIDONGNETV): 영상 콘텐츠 업로드
|
||||||
- **링고비즈**: 연결음 업데이트
|
- **링고비즈** (RINGOBIZ): 연결음 업데이트
|
||||||
- **지니TV**: 광고 등록
|
- **지니TV** (GINITV): 광고 등록
|
||||||
- **SNS**: Instagram, Naver Blog, Kakao Channel
|
- **SNS**: Instagram (INSTAGRAM), Naver Blog (NAVER), Kakao Channel (KAKAO)
|
||||||
|
|
||||||
## Resilience 패턴
|
## Resilience 패턴
|
||||||
- Circuit Breaker: 채널별 독립적 장애 격리
|
- Circuit Breaker: 채널별 독립적 장애 격리
|
||||||
@ -79,23 +79,21 @@ paths:
|
|||||||
summary: 다중 채널 배포 예시
|
summary: 다중 채널 배포 예시
|
||||||
value:
|
value:
|
||||||
eventId: "evt-12345"
|
eventId: "evt-12345"
|
||||||
|
title: "신규 고객 환영 이벤트"
|
||||||
|
description: "신규 고객님을 위한 특별 할인 이벤트"
|
||||||
|
imageUrl: "https://cdn.example.com/images/event-main.jpg"
|
||||||
channels:
|
channels:
|
||||||
- type: "WOORIDONGNE_TV"
|
- "URIDONGNETV"
|
||||||
config:
|
- "INSTAGRAM"
|
||||||
|
- "NAVER"
|
||||||
|
channelSettings:
|
||||||
|
URIDONGNETV:
|
||||||
radius: "1km"
|
radius: "1km"
|
||||||
timeSlots:
|
timeSlot: "evening"
|
||||||
- "weekday_evening"
|
INSTAGRAM:
|
||||||
- "weekend_lunch"
|
scheduledTime: "2025-11-01T10:00:00"
|
||||||
- type: "INSTAGRAM"
|
NAVER:
|
||||||
config:
|
scheduledTime: "2025-11-01T10:30:00"
|
||||||
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"
|
|
||||||
responses:
|
responses:
|
||||||
'200':
|
'200':
|
||||||
description: 배포 완료
|
description: 배포 완료
|
||||||
@ -107,25 +105,29 @@ paths:
|
|||||||
allSuccess:
|
allSuccess:
|
||||||
summary: 모든 채널 배포 성공
|
summary: 모든 채널 배포 성공
|
||||||
value:
|
value:
|
||||||
distributionId: "dist-12345"
|
|
||||||
eventId: "evt-12345"
|
eventId: "evt-12345"
|
||||||
status: "COMPLETED"
|
success: true
|
||||||
completedAt: "2025-11-01T09:00:00Z"
|
channelResults:
|
||||||
results:
|
- channel: "URIDONGNETV"
|
||||||
- channel: "WOORIDONGNE_TV"
|
success: true
|
||||||
status: "SUCCESS"
|
|
||||||
distributionId: "wtv-uuid-12345"
|
distributionId: "wtv-uuid-12345"
|
||||||
estimatedViews: 1000
|
estimatedReach: 1000
|
||||||
message: "배포 완료"
|
executionTimeMs: 234
|
||||||
- channel: "INSTAGRAM"
|
- channel: "INSTAGRAM"
|
||||||
status: "SUCCESS"
|
success: true
|
||||||
postUrl: "https://instagram.com/p/generated-post-id"
|
distributionId: "ig-uuid-12345"
|
||||||
postId: "ig-post-12345"
|
estimatedReach: 500
|
||||||
message: "게시 완료"
|
executionTimeMs: 456
|
||||||
- channel: "NAVER_BLOG"
|
- channel: "NAVER"
|
||||||
status: "SUCCESS"
|
success: true
|
||||||
postUrl: "https://blog.naver.com/store123/generated-post"
|
distributionId: "naver-uuid-12345"
|
||||||
message: "게시 완료"
|
estimatedReach: 300
|
||||||
|
executionTimeMs: 123
|
||||||
|
successCount: 3
|
||||||
|
failureCount: 0
|
||||||
|
completedAt: "2025-11-01T09:00:00"
|
||||||
|
totalExecutionTimeMs: 1234
|
||||||
|
message: "배포가 성공적으로 완료되었습니다"
|
||||||
'400':
|
'400':
|
||||||
description: 잘못된 요청
|
description: 잘못된 요청
|
||||||
content:
|
content:
|
||||||
@ -217,67 +219,77 @@ paths:
|
|||||||
value:
|
value:
|
||||||
eventId: "evt-12345"
|
eventId: "evt-12345"
|
||||||
overallStatus: "COMPLETED"
|
overallStatus: "COMPLETED"
|
||||||
completedAt: "2025-11-01T09:00:00Z"
|
startedAt: "2025-11-01T08:58:00"
|
||||||
|
completedAt: "2025-11-01T09:00:00"
|
||||||
channels:
|
channels:
|
||||||
- channel: "WOORIDONGNE_TV"
|
- channel: "URIDONGNETV"
|
||||||
status: "COMPLETED"
|
status: "COMPLETED"
|
||||||
distributionId: "wtv-uuid-12345"
|
distributionId: "wtv-uuid-12345"
|
||||||
estimatedViews: 1500
|
estimatedViews: 1500
|
||||||
completedAt: "2025-11-01T09:00:00Z"
|
completedAt: "2025-11-01T09:00:00"
|
||||||
- channel: "RINGO_BIZ"
|
- channel: "RINGOBIZ"
|
||||||
status: "COMPLETED"
|
status: "COMPLETED"
|
||||||
updateTimestamp: "2025-11-01T09:00:00Z"
|
updateTimestamp: "2025-11-01T09:00:00"
|
||||||
- channel: "GENIE_TV"
|
completedAt: "2025-11-01T09:00:00"
|
||||||
|
- channel: "GINITV"
|
||||||
status: "COMPLETED"
|
status: "COMPLETED"
|
||||||
adId: "gtv-uuid-12345"
|
adId: "gtv-uuid-12345"
|
||||||
impressionSchedule:
|
impressionSchedule:
|
||||||
- "2025-11-01 18:00-20:00"
|
- "2025-11-01 18:00-20:00"
|
||||||
- "2025-11-02 12:00-14:00"
|
- "2025-11-02 12:00-14:00"
|
||||||
|
completedAt: "2025-11-01T09:00:00"
|
||||||
- channel: "INSTAGRAM"
|
- channel: "INSTAGRAM"
|
||||||
status: "COMPLETED"
|
status: "COMPLETED"
|
||||||
postUrl: "https://instagram.com/p/generated-post-id"
|
postUrl: "https://instagram.com/p/generated-post-id"
|
||||||
postId: "ig-post-12345"
|
postId: "ig-post-12345"
|
||||||
- channel: "NAVER_BLOG"
|
completedAt: "2025-11-01T09:00:00"
|
||||||
|
- channel: "NAVER"
|
||||||
status: "COMPLETED"
|
status: "COMPLETED"
|
||||||
postUrl: "https://blog.naver.com/store123/generated-post"
|
postUrl: "https://blog.naver.com/store123/generated-post"
|
||||||
- channel: "KAKAO_CHANNEL"
|
completedAt: "2025-11-01T09:00:00"
|
||||||
|
- channel: "KAKAO"
|
||||||
status: "COMPLETED"
|
status: "COMPLETED"
|
||||||
messageId: "kakao-msg-12345"
|
messageId: "kakao-msg-12345"
|
||||||
|
completedAt: "2025-11-01T09:00:00"
|
||||||
inProgress:
|
inProgress:
|
||||||
summary: 배포 진행중 상태
|
summary: 배포 진행중 상태
|
||||||
value:
|
value:
|
||||||
eventId: "evt-12345"
|
eventId: "evt-12345"
|
||||||
overallStatus: "IN_PROGRESS"
|
overallStatus: "IN_PROGRESS"
|
||||||
startedAt: "2025-11-01T08:58:00Z"
|
startedAt: "2025-11-01T08:58:00"
|
||||||
channels:
|
channels:
|
||||||
- channel: "WOORIDONGNE_TV"
|
- channel: "URIDONGNETV"
|
||||||
status: "COMPLETED"
|
status: "COMPLETED"
|
||||||
distributionId: "wtv-uuid-12345"
|
distributionId: "wtv-uuid-12345"
|
||||||
estimatedViews: 1500
|
estimatedViews: 1500
|
||||||
|
completedAt: "2025-11-01T08:59:00"
|
||||||
- channel: "INSTAGRAM"
|
- channel: "INSTAGRAM"
|
||||||
status: "IN_PROGRESS"
|
status: "IN_PROGRESS"
|
||||||
progress: 50
|
progress: 50
|
||||||
- channel: "NAVER_BLOG"
|
- channel: "NAVER"
|
||||||
status: "PENDING"
|
status: "PENDING"
|
||||||
partialFailure:
|
partialFailure:
|
||||||
summary: 일부 채널 실패 상태
|
summary: 일부 채널 실패 상태
|
||||||
value:
|
value:
|
||||||
eventId: "evt-12345"
|
eventId: "evt-12345"
|
||||||
overallStatus: "PARTIAL_FAILURE"
|
overallStatus: "PARTIAL_FAILURE"
|
||||||
completedAt: "2025-11-01T09:00:00Z"
|
startedAt: "2025-11-01T08:58:00"
|
||||||
|
completedAt: "2025-11-01T09:00:00"
|
||||||
channels:
|
channels:
|
||||||
- channel: "WOORIDONGNE_TV"
|
- channel: "URIDONGNETV"
|
||||||
status: "COMPLETED"
|
status: "COMPLETED"
|
||||||
distributionId: "wtv-uuid-12345"
|
distributionId: "wtv-uuid-12345"
|
||||||
estimatedViews: 1500
|
estimatedViews: 1500
|
||||||
|
completedAt: "2025-11-01T08:59:00"
|
||||||
- channel: "INSTAGRAM"
|
- channel: "INSTAGRAM"
|
||||||
status: "FAILED"
|
status: "FAILED"
|
||||||
errorMessage: "Instagram API 타임아웃"
|
errorMessage: "Instagram API 타임아웃"
|
||||||
retries: 3
|
retries: 3
|
||||||
lastRetryAt: "2025-11-01T08:59:30Z"
|
lastRetryAt: "2025-11-01T08:59:30"
|
||||||
- channel: "NAVER_BLOG"
|
- channel: "NAVER"
|
||||||
status: "COMPLETED"
|
status: "COMPLETED"
|
||||||
postUrl: "https://blog.naver.com/store123/generated-post"
|
postUrl: "https://blog.naver.com/store123/generated-post"
|
||||||
|
completedAt: "2025-11-01T09:00:00"
|
||||||
'404':
|
'404':
|
||||||
description: 배포 이력을 찾을 수 없음
|
description: 배포 이력을 찾을 수 없음
|
||||||
content:
|
content:
|
||||||
@ -305,196 +317,133 @@ components:
|
|||||||
required:
|
required:
|
||||||
- eventId
|
- eventId
|
||||||
- channels
|
- channels
|
||||||
- contentUrls
|
|
||||||
properties:
|
properties:
|
||||||
eventId:
|
eventId:
|
||||||
type: string
|
type: string
|
||||||
description: 이벤트 ID
|
description: 이벤트 ID
|
||||||
example: "evt-12345"
|
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:
|
channels:
|
||||||
type: array
|
type: array
|
||||||
description: 배포할 채널 목록
|
description: 배포할 채널 목록
|
||||||
minItems: 1
|
minItems: 1
|
||||||
items:
|
items:
|
||||||
$ref: '#/components/schemas/ChannelConfig'
|
|
||||||
contentUrls:
|
|
||||||
type: object
|
|
||||||
description: 플랫폼별 콘텐츠 URL
|
|
||||||
properties:
|
|
||||||
wooridongneTV:
|
|
||||||
type: string
|
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:
|
enum:
|
||||||
- WOORIDONGNE_TV
|
- URIDONGNETV
|
||||||
- RINGO_BIZ
|
- RINGOBIZ
|
||||||
- GENIE_TV
|
- GINITV
|
||||||
- INSTAGRAM
|
- INSTAGRAM
|
||||||
- NAVER_BLOG
|
- NAVER
|
||||||
- KAKAO_CHANNEL
|
- KAKAO
|
||||||
example: "INSTAGRAM"
|
example: ["URIDONGNETV", "INSTAGRAM", "NAVER"]
|
||||||
config:
|
channelSettings:
|
||||||
|
type: object
|
||||||
|
description: 채널별 추가 설정 (Optional)
|
||||||
|
additionalProperties:
|
||||||
type: object
|
type: object
|
||||||
description: 채널별 설정 (채널에 따라 다름)
|
|
||||||
additionalProperties: true
|
additionalProperties: true
|
||||||
example:
|
example:
|
||||||
scheduledTime: "2025-11-01T10:00:00Z"
|
URIDONGNETV:
|
||||||
caption: "이벤트 안내"
|
radius: "1km"
|
||||||
hashtags:
|
timeSlot: "evening"
|
||||||
- "이벤트"
|
INSTAGRAM:
|
||||||
- "할인"
|
scheduledTime: "2025-11-01T10:00:00"
|
||||||
|
|
||||||
DistributionResponse:
|
DistributionResponse:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- distributionId
|
|
||||||
- eventId
|
- eventId
|
||||||
- status
|
- success
|
||||||
- results
|
- channelResults
|
||||||
|
- successCount
|
||||||
|
- failureCount
|
||||||
properties:
|
properties:
|
||||||
distributionId:
|
|
||||||
type: string
|
|
||||||
description: 배포 ID
|
|
||||||
example: "dist-12345"
|
|
||||||
eventId:
|
eventId:
|
||||||
type: string
|
type: string
|
||||||
description: 이벤트 ID
|
description: 이벤트 ID
|
||||||
example: "evt-12345"
|
example: "evt-12345"
|
||||||
status:
|
success:
|
||||||
type: string
|
type: boolean
|
||||||
description: 전체 배포 상태
|
description: 배포 성공 여부 (모든 채널 또는 일부 채널 성공)
|
||||||
enum:
|
example: true
|
||||||
- PENDING
|
channelResults:
|
||||||
- IN_PROGRESS
|
type: array
|
||||||
- COMPLETED
|
description: 채널별 배포 결과
|
||||||
- PARTIAL_FAILURE
|
items:
|
||||||
- FAILED
|
$ref: '#/components/schemas/ChannelDistributionResult'
|
||||||
example: "COMPLETED"
|
successCount:
|
||||||
startedAt:
|
type: integer
|
||||||
type: string
|
description: 성공한 채널 수
|
||||||
format: date-time
|
example: 3
|
||||||
description: 배포 시작 시각
|
failureCount:
|
||||||
example: "2025-11-01T08:59:00Z"
|
type: integer
|
||||||
|
description: 실패한 채널 수
|
||||||
|
example: 0
|
||||||
completedAt:
|
completedAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
description: 배포 완료 시각
|
description: 배포 완료 시각
|
||||||
example: "2025-11-01T09:00:00Z"
|
example: "2025-11-01T09:00:00"
|
||||||
results:
|
totalExecutionTimeMs:
|
||||||
type: array
|
type: integer
|
||||||
description: 채널별 배포 결과
|
format: int64
|
||||||
items:
|
description: 전체 배포 소요 시간 (ms)
|
||||||
$ref: '#/components/schemas/ChannelResult'
|
example: 1234
|
||||||
|
message:
|
||||||
|
type: string
|
||||||
|
description: 메시지
|
||||||
|
example: "배포가 성공적으로 완료되었습니다"
|
||||||
|
|
||||||
ChannelResult:
|
ChannelDistributionResult:
|
||||||
type: object
|
type: object
|
||||||
required:
|
required:
|
||||||
- channel
|
- channel
|
||||||
- status
|
- success
|
||||||
properties:
|
properties:
|
||||||
channel:
|
channel:
|
||||||
type: string
|
type: string
|
||||||
description: 채널 타입
|
description: 채널 타입
|
||||||
enum:
|
enum:
|
||||||
- WOORIDONGNE_TV
|
- URIDONGNETV
|
||||||
- RINGO_BIZ
|
- RINGOBIZ
|
||||||
- GENIE_TV
|
- GINITV
|
||||||
- INSTAGRAM
|
- INSTAGRAM
|
||||||
- NAVER_BLOG
|
- NAVER
|
||||||
- KAKAO_CHANNEL
|
- KAKAO
|
||||||
example: "INSTAGRAM"
|
example: "INSTAGRAM"
|
||||||
status:
|
success:
|
||||||
type: string
|
type: boolean
|
||||||
description: 채널별 배포 상태
|
description: 배포 성공 여부
|
||||||
enum:
|
example: true
|
||||||
- PENDING
|
|
||||||
- IN_PROGRESS
|
|
||||||
- SUCCESS
|
|
||||||
- FAILED
|
|
||||||
example: "SUCCESS"
|
|
||||||
distributionId:
|
distributionId:
|
||||||
type: string
|
type: string
|
||||||
description: 채널별 배포 ID (우리동네TV, 지니TV)
|
description: 배포 ID (성공 시)
|
||||||
example: "wtv-uuid-12345"
|
example: "dist-uuid-12345"
|
||||||
estimatedViews:
|
estimatedReach:
|
||||||
type: integer
|
type: integer
|
||||||
description: 예상 노출 수 (우리동네TV, 지니TV)
|
description: 예상 노출 수 (성공 시)
|
||||||
example: 1500
|
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:
|
errorMessage:
|
||||||
type: string
|
type: string
|
||||||
description: 오류 메시지 (실패 시)
|
description: 에러 메시지 (실패 시)
|
||||||
example: "Instagram API 타임아웃"
|
example: "Instagram API 타임아웃"
|
||||||
retries:
|
executionTimeMs:
|
||||||
type: integer
|
type: integer
|
||||||
description: 재시도 횟수
|
format: int64
|
||||||
example: 0
|
description: 배포 소요 시간 (ms)
|
||||||
lastRetryAt:
|
example: 234
|
||||||
type: string
|
|
||||||
format: date-time
|
|
||||||
description: 마지막 재시도 시각
|
|
||||||
example: "2025-11-01T08:59:30Z"
|
|
||||||
|
|
||||||
DistributionStatusResponse:
|
DistributionStatusResponse:
|
||||||
type: object
|
type: object
|
||||||
@ -544,12 +493,12 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
description: 채널 타입
|
description: 채널 타입
|
||||||
enum:
|
enum:
|
||||||
- WOORIDONGNE_TV
|
- URIDONGNETV
|
||||||
- RINGO_BIZ
|
- RINGOBIZ
|
||||||
- GENIE_TV
|
- GINITV
|
||||||
- INSTAGRAM
|
- INSTAGRAM
|
||||||
- NAVER_BLOG
|
- NAVER
|
||||||
- KAKAO_CHANNEL
|
- KAKAO
|
||||||
example: "INSTAGRAM"
|
example: "INSTAGRAM"
|
||||||
status:
|
status:
|
||||||
type: string
|
type: string
|
||||||
@ -569,7 +518,7 @@ components:
|
|||||||
distributionId:
|
distributionId:
|
||||||
type: string
|
type: string
|
||||||
description: 채널별 배포 ID
|
description: 채널별 배포 ID
|
||||||
example: "wtv-uuid-12345"
|
example: "dist-uuid-12345"
|
||||||
estimatedViews:
|
estimatedViews:
|
||||||
type: integer
|
type: integer
|
||||||
description: 예상 노출 수
|
description: 예상 노출 수
|
||||||
@ -578,35 +527,35 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
description: 업데이트 완료 시각
|
description: 업데이트 완료 시각
|
||||||
example: "2025-11-01T09:00:00Z"
|
example: "2025-11-01T09:00:00"
|
||||||
adId:
|
adId:
|
||||||
type: string
|
type: string
|
||||||
description: 광고 ID
|
description: 광고 ID (지니TV)
|
||||||
example: "gtv-uuid-12345"
|
example: "gtv-uuid-12345"
|
||||||
impressionSchedule:
|
impressionSchedule:
|
||||||
type: array
|
type: array
|
||||||
description: 노출 스케줄
|
description: 노출 스케줄 (지니TV)
|
||||||
items:
|
items:
|
||||||
type: string
|
type: string
|
||||||
example:
|
example:
|
||||||
- "2025-11-01 18:00-20:00"
|
- "2025-11-01 18:00-20:00"
|
||||||
postUrl:
|
postUrl:
|
||||||
type: string
|
type: string
|
||||||
description: 게시물 URL
|
description: 게시물 URL (Instagram, Naver Blog)
|
||||||
example: "https://instagram.com/p/generated-post-id"
|
example: "https://instagram.com/p/generated-post-id"
|
||||||
postId:
|
postId:
|
||||||
type: string
|
type: string
|
||||||
description: 게시물 ID
|
description: 게시물 ID (Instagram)
|
||||||
example: "ig-post-12345"
|
example: "ig-post-12345"
|
||||||
messageId:
|
messageId:
|
||||||
type: string
|
type: string
|
||||||
description: 메시지 ID
|
description: 메시지 ID (Kakao Channel)
|
||||||
example: "kakao-msg-12345"
|
example: "kakao-msg-12345"
|
||||||
completedAt:
|
completedAt:
|
||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
description: 완료 시각
|
description: 완료 시각
|
||||||
example: "2025-11-01T09:00:00Z"
|
example: "2025-11-01T09:00:00"
|
||||||
errorMessage:
|
errorMessage:
|
||||||
type: string
|
type: string
|
||||||
description: 오류 메시지
|
description: 오류 메시지
|
||||||
@ -619,7 +568,7 @@ components:
|
|||||||
type: string
|
type: string
|
||||||
format: date-time
|
format: date-time
|
||||||
description: 마지막 재시도 시각
|
description: 마지막 재시도 시각
|
||||||
example: "2025-11-01T08:59:30Z"
|
example: "2025-11-01T08:59:30"
|
||||||
|
|
||||||
ErrorResponse:
|
ErrorResponse:
|
||||||
type: object
|
type: object
|
||||||
|
|||||||
@ -19,7 +19,7 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/distribution")
|
@RequestMapping("/distribution")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
public class DistributionController {
|
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;
|
package com.kt.distribution.repository;
|
||||||
|
|
||||||
import com.kt.distribution.dto.DistributionStatusResponse;
|
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 lombok.extern.slf4j.Slf4j;
|
||||||
import org.springframework.stereotype.Repository;
|
import org.springframework.stereotype.Repository;
|
||||||
|
import org.springframework.transaction.annotation.Transactional;
|
||||||
|
|
||||||
import java.util.Map;
|
|
||||||
import java.util.Optional;
|
import java.util.Optional;
|
||||||
import java.util.concurrent.ConcurrentHashMap;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배포 상태 저장소
|
* 배포 상태 저장소
|
||||||
*
|
*
|
||||||
* 메모리 기반으로 배포 상태를 관리합니다.
|
* PostgreSQL 데이터베이스를 사용하여 배포 상태를 영구 저장합니다.
|
||||||
* 실제 운영 환경에서는 Redis 또는 데이터베이스를 사용하여 영구 저장하는 것을 권장합니다.
|
*
|
||||||
|
* @author Backend Developer
|
||||||
|
* @since 2025-10-24
|
||||||
*/
|
*/
|
||||||
@Slf4j
|
@Slf4j
|
||||||
@Repository
|
@Repository
|
||||||
|
@RequiredArgsConstructor
|
||||||
public class DistributionStatusRepository {
|
public class DistributionStatusRepository {
|
||||||
|
|
||||||
/**
|
private final DistributionStatusJpaRepository jpaRepository;
|
||||||
* 이벤트 ID를 키로 배포 상태를 저장하는 메모리 저장소
|
private final DistributionStatusMapper mapper;
|
||||||
*/
|
|
||||||
private final Map<String, DistributionStatusResponse> distributionStatuses = new ConcurrentHashMap<>();
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 배포 상태 저장
|
* 배포 상태 저장
|
||||||
*
|
*
|
||||||
* @param eventId 이벤트 ID
|
* @param eventId 이벤트 ID
|
||||||
* @param status 배포 상태
|
* @param status 배포 상태 DTO
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
public void save(String eventId, DistributionStatusResponse status) {
|
public void save(String eventId, DistributionStatusResponse status) {
|
||||||
log.debug("Saving distribution status: eventId={}, overallStatus={}", eventId, status.getOverallStatus());
|
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
|
* @param eventId 이벤트 ID
|
||||||
* @return 배포 상태 (없으면 Optional.empty())
|
* @return 배포 상태 DTO (없으면 Optional.empty())
|
||||||
*/
|
*/
|
||||||
|
@Transactional(readOnly = true)
|
||||||
public Optional<DistributionStatusResponse> findByEventId(String eventId) {
|
public Optional<DistributionStatusResponse> findByEventId(String eventId) {
|
||||||
log.debug("Finding distribution status: eventId={}", 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
|
* @param eventId 이벤트 ID
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
public void delete(String eventId) {
|
public void delete(String eventId) {
|
||||||
log.debug("Deleting distribution status: eventId={}", eventId);
|
log.debug("Deleting distribution status: eventId={}", eventId);
|
||||||
distributionStatuses.remove(eventId);
|
jpaRepository.deleteByEventId(eventId);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 모든 배포 상태 삭제 (테스트용)
|
* 모든 배포 상태 삭제 (테스트용)
|
||||||
*/
|
*/
|
||||||
|
@Transactional
|
||||||
public void deleteAll() {
|
public void deleteAll() {
|
||||||
log.debug("Deleting all distribution statuses");
|
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