From b71d27aa8bf0c8872f23ce610e9fc0829510ef09 Mon Sep 17 00:00:00 2001 From: merrycoral Date: Wed, 29 Oct 2025 20:54:10 +0900 Subject: [PATCH] =?UTF-8?q?=EB=B9=84=EC=A6=88=EB=8B=88=EC=8A=A4=20?= =?UTF-8?q?=EC=B9=9C=ED=99=94=EC=A0=81=20eventId=20=EB=B0=8F=20jobId=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=A1=9C=EC=A7=81=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - EventIdGenerator 추가: EVT-{storeId}-{yyyyMMddHHmmss}-{random8} 형식 - JobIdGenerator 추가: JOB-{type}-{timestamp}-{random8} 형식 - EventService, JobService에 Generator 주입 및 사용 - AIJobKafkaProducer에 eventId 및 메시지 필드 추가 - AIEventGenerationJobMessage DTO 필드 확장 - Javadoc에서 UUID 표현 제거 및 실제 형식 명시 - Event.java의 UUID 백업 생성 로직 제거 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- .../kafka/AIEventGenerationJobMessage.java | 30 +++++ .../application/service/EventIdGenerator.java | 112 ++++++++++++++++ .../application/service/EventService.java | 15 +++ .../application/service/JobIdGenerator.java | 123 ++++++++++++++++++ .../application/service/JobService.java | 6 + .../kafka/AIJobKafkaProducer.java | 11 +- .../kafka/ImageJobKafkaProducer.java | 6 +- 7 files changed, 297 insertions(+), 6 deletions(-) create mode 100644 event-service/src/main/java/com/kt/event/eventservice/application/service/EventIdGenerator.java create mode 100644 event-service/src/main/java/com/kt/event/eventservice/application/service/JobIdGenerator.java diff --git a/event-service/src/main/java/com/kt/event/eventservice/application/dto/kafka/AIEventGenerationJobMessage.java b/event-service/src/main/java/com/kt/event/eventservice/application/dto/kafka/AIEventGenerationJobMessage.java index 7d8b2fe..b089f6b 100644 --- a/event-service/src/main/java/com/kt/event/eventservice/application/dto/kafka/AIEventGenerationJobMessage.java +++ b/event-service/src/main/java/com/kt/event/eventservice/application/dto/kafka/AIEventGenerationJobMessage.java @@ -32,6 +32,36 @@ public class AIEventGenerationJobMessage { @JsonProperty("user_id") private String userId; + /** + * 이벤트 ID + */ + @JsonProperty("event_id") + private String eventId; + + /** + * 매장명 + */ + @JsonProperty("store_name") + private String storeName; + + /** + * 매장 업종 + */ + @JsonProperty("store_category") + private String storeCategory; + + /** + * 매장 설명 + */ + @JsonProperty("store_description") + private String storeDescription; + + /** + * 이벤트 목적 + */ + @JsonProperty("objective") + private String objective; + /** * 작업 상태 (PENDING, PROCESSING, COMPLETED, FAILED) */ diff --git a/event-service/src/main/java/com/kt/event/eventservice/application/service/EventIdGenerator.java b/event-service/src/main/java/com/kt/event/eventservice/application/service/EventIdGenerator.java new file mode 100644 index 0000000..ceb0939 --- /dev/null +++ b/event-service/src/main/java/com/kt/event/eventservice/application/service/EventIdGenerator.java @@ -0,0 +1,112 @@ +package com.kt.event.eventservice.application.service; + +import org.springframework.stereotype.Component; + +import java.time.LocalDateTime; +import java.time.format.DateTimeFormatter; +import java.util.UUID; + +/** + * 이벤트 ID 생성기 + * + * 비즈니스 친화적인 eventId를 생성합니다. + * 형식: EVT-{storeId}-{yyyyMMddHHmmss}-{random8} + * 예시: EVT-store123-20251029143025-a1b2c3d4 + * + * VARCHAR(50) 길이 제약사항을 고려하여 설계되었습니다. + */ +@Component +public class EventIdGenerator { + + private static final String PREFIX = "EVT"; + private static final DateTimeFormatter TIMESTAMP_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMddHHmmss"); + private static final int RANDOM_LENGTH = 8; + + /** + * 이벤트 ID 생성 + * + * @param storeId 상점 ID (최대 15자 권장) + * @return 생성된 이벤트 ID + * @throws IllegalArgumentException storeId가 null이거나 비어있는 경우 + */ + public String generate(String storeId) { + if (storeId == null || storeId.isBlank()) { + throw new IllegalArgumentException("storeId는 필수입니다"); + } + + // storeId 길이 검증 (전체 길이 50자 제한) + if (storeId.length() > 15) { + throw new IllegalArgumentException("storeId는 15자 이하여야 합니다"); + } + + String timestamp = LocalDateTime.now().format(TIMESTAMP_FORMATTER); + String randomPart = generateRandomPart(); + + // 형식: EVT-{storeId}-{timestamp}-{random} + // 예상 길이: 3 + 1 + 15 + 1 + 14 + 1 + 8 = 43자 (최대) + String eventId = String.format("%s-%s-%s-%s", PREFIX, storeId, timestamp, randomPart); + + // 길이 검증 + if (eventId.length() > 50) { + throw new IllegalStateException( + String.format("생성된 eventId 길이(%d)가 50자를 초과했습니다: %s", + eventId.length(), eventId) + ); + } + + return eventId; + } + + /** + * UUID 기반 랜덤 문자열 생성 + * + * @return 8자리 랜덤 문자열 (소문자 영숫자) + */ + private String generateRandomPart() { + return UUID.randomUUID() + .toString() + .replace("-", "") + .substring(0, RANDOM_LENGTH) + .toLowerCase(); + } + + /** + * eventId 형식 검증 + * + * @param eventId 검증할 이벤트 ID + * @return 유효하면 true, 아니면 false + */ + public boolean isValid(String eventId) { + if (eventId == null || eventId.isBlank()) { + return false; + } + + // EVT-로 시작하는지 확인 + if (!eventId.startsWith(PREFIX + "-")) { + return false; + } + + // 길이 검증 + if (eventId.length() > 50) { + return false; + } + + // 형식 검증: EVT-{storeId}-{14자리숫자}-{8자리영숫자} + String[] parts = eventId.split("-"); + if (parts.length != 4) { + return false; + } + + // timestamp 부분이 14자리 숫자인지 확인 + if (parts[2].length() != 14 || !parts[2].matches("\\d{14}")) { + return false; + } + + // random 부분이 8자리 영숫자인지 확인 + if (parts[3].length() != 8 || !parts[3].matches("[a-z0-9]{8}")) { + return false; + } + + return true; + } +} diff --git a/event-service/src/main/java/com/kt/event/eventservice/application/service/EventService.java b/event-service/src/main/java/com/kt/event/eventservice/application/service/EventService.java index b7b552d..2db00bd 100644 --- a/event-service/src/main/java/com/kt/event/eventservice/application/service/EventService.java +++ b/event-service/src/main/java/com/kt/event/eventservice/application/service/EventService.java @@ -47,6 +47,8 @@ public class EventService { private final AIJobKafkaProducer aiJobKafkaProducer; private final ImageJobKafkaProducer imageJobKafkaProducer; private final EventKafkaProducer eventKafkaProducer; + private final EventIdGenerator eventIdGenerator; + private final JobIdGenerator jobIdGenerator; /** * 이벤트 생성 (Step 1: 목적 선택) @@ -61,8 +63,13 @@ public class EventService { log.info("이벤트 생성 시작 - userId: {}, storeId: {}, objective: {}", userId, storeId, request.getObjective()); + // eventId 생성 + String eventId = eventIdGenerator.generate(storeId); + log.info("생성된 eventId: {}", eventId); + // 이벤트 엔티티 생성 Event event = Event.builder() + .eventId(eventId) .userId(userId) .storeId(storeId) .objective(request.getObjective()) @@ -235,7 +242,11 @@ public class EventService { String.join(", ", request.getPlatforms())); // Job 엔티티 생성 + String jobId = jobIdGenerator.generate(JobType.IMAGE_GENERATION); + log.info("생성된 jobId: {}", jobId); + Job job = Job.builder() + .jobId(jobId) .eventId(eventId) .jobType(JobType.IMAGE_GENERATION) .build(); @@ -312,7 +323,11 @@ public class EventService { } // Job 엔티티 생성 + String jobId = jobIdGenerator.generate(JobType.AI_RECOMMENDATION); + log.info("생성된 jobId: {}", jobId); + Job job = Job.builder() + .jobId(jobId) .eventId(eventId) .jobType(JobType.AI_RECOMMENDATION) .build(); diff --git a/event-service/src/main/java/com/kt/event/eventservice/application/service/JobIdGenerator.java b/event-service/src/main/java/com/kt/event/eventservice/application/service/JobIdGenerator.java new file mode 100644 index 0000000..04437f8 --- /dev/null +++ b/event-service/src/main/java/com/kt/event/eventservice/application/service/JobIdGenerator.java @@ -0,0 +1,123 @@ +package com.kt.event.eventservice.application.service; + +import com.kt.event.eventservice.domain.enums.JobType; +import org.springframework.stereotype.Component; + +import java.util.UUID; + +/** + * Job ID 생성기 + * + * 비즈니스 친화적인 jobId를 생성합니다. + * 형식: JOB-{jobType}-{timestamp}-{random8} + * 예시: JOB-AI-20251029143025-a1b2c3d4 + * + * VARCHAR(50) 길이 제약사항을 고려하여 설계되었습니다. + */ +@Component +public class JobIdGenerator { + + private static final String PREFIX = "JOB"; + private static final int RANDOM_LENGTH = 8; + + /** + * Job ID 생성 + * + * @param jobType Job 타입 + * @return 생성된 Job ID + * @throws IllegalArgumentException jobType이 null인 경우 + */ + public String generate(JobType jobType) { + if (jobType == null) { + throw new IllegalArgumentException("jobType은 필수입니다"); + } + + String typeCode = getTypeCode(jobType); + String timestamp = String.valueOf(System.currentTimeMillis()); + String randomPart = generateRandomPart(); + + // 형식: JOB-{type}-{timestamp}-{random} + // 예상 길이: 3 + 1 + 5 + 1 + 13 + 1 + 8 = 32자 (최대) + String jobId = String.format("%s-%s-%s-%s", PREFIX, typeCode, timestamp, randomPart); + + // 길이 검증 + if (jobId.length() > 50) { + throw new IllegalStateException( + String.format("생성된 jobId 길이(%d)가 50자를 초과했습니다: %s", + jobId.length(), jobId) + ); + } + + return jobId; + } + + /** + * JobType을 짧은 코드로 변환 + * + * @param jobType Job 타입 + * @return 타입 코드 + */ + private String getTypeCode(JobType jobType) { + switch (jobType) { + case AI_RECOMMENDATION: + return "AI"; + case IMAGE_GENERATION: + return "IMG"; + default: + return jobType.name().substring(0, Math.min(5, jobType.name().length())); + } + } + + /** + * UUID 기반 랜덤 문자열 생성 + * + * @return 8자리 랜덤 문자열 (소문자 영숫자) + */ + private String generateRandomPart() { + return UUID.randomUUID() + .toString() + .replace("-", "") + .substring(0, RANDOM_LENGTH) + .toLowerCase(); + } + + /** + * jobId 형식 검증 + * + * @param jobId 검증할 Job ID + * @return 유효하면 true, 아니면 false + */ + public boolean isValid(String jobId) { + if (jobId == null || jobId.isBlank()) { + return false; + } + + // JOB-로 시작하는지 확인 + if (!jobId.startsWith(PREFIX + "-")) { + return false; + } + + // 길이 검증 + if (jobId.length() > 50) { + return false; + } + + // 형식 검증: JOB-{type}-{timestamp}-{8자리영숫자} + String[] parts = jobId.split("-"); + if (parts.length != 4) { + return false; + } + + // timestamp 부분이 숫자인지 확인 + if (!parts[2].matches("\\d+")) { + return false; + } + + // random 부분이 8자리 영숫자인지 확인 + if (parts[3].length() != 8 || !parts[3].matches("[a-z0-9]{8}")) { + return false; + } + + return true; + } +} diff --git a/event-service/src/main/java/com/kt/event/eventservice/application/service/JobService.java b/event-service/src/main/java/com/kt/event/eventservice/application/service/JobService.java index c98c7fe..317c271 100644 --- a/event-service/src/main/java/com/kt/event/eventservice/application/service/JobService.java +++ b/event-service/src/main/java/com/kt/event/eventservice/application/service/JobService.java @@ -27,6 +27,7 @@ import org.springframework.transaction.annotation.Transactional; public class JobService { private final JobRepository jobRepository; + private final JobIdGenerator jobIdGenerator; /** * Job 생성 @@ -39,7 +40,12 @@ public class JobService { public Job createJob(String eventId, JobType jobType) { log.info("Job 생성 - eventId: {}, jobType: {}", eventId, jobType); + // jobId 생성 + String jobId = jobIdGenerator.generate(jobType); + log.info("생성된 jobId: {}", jobId); + Job job = Job.builder() + .jobId(jobId) .eventId(eventId) .jobType(jobType) .build(); diff --git a/event-service/src/main/java/com/kt/event/eventservice/infrastructure/kafka/AIJobKafkaProducer.java b/event-service/src/main/java/com/kt/event/eventservice/infrastructure/kafka/AIJobKafkaProducer.java index 05f179f..1f82ebe 100644 --- a/event-service/src/main/java/com/kt/event/eventservice/infrastructure/kafka/AIJobKafkaProducer.java +++ b/event-service/src/main/java/com/kt/event/eventservice/infrastructure/kafka/AIJobKafkaProducer.java @@ -35,9 +35,9 @@ public class AIJobKafkaProducer { /** * AI 이벤트 생성 작업 메시지 발행 * - * @param jobId 작업 ID (UUID String) - * @param userId 사용자 ID (UUID String) - * @param eventId 이벤트 ID (UUID String) + * @param jobId 작업 ID (JOB-{type}-{timestamp}-{random8}) + * @param userId 사용자 ID + * @param eventId 이벤트 ID (EVT-{storeId}-{yyyyMMddHHmmss}-{random8}) * @param storeName 매장명 * @param storeCategory 매장 업종 * @param storeDescription 매장 설명 @@ -55,6 +55,11 @@ public class AIJobKafkaProducer { AIEventGenerationJobMessage message = AIEventGenerationJobMessage.builder() .jobId(jobId) .userId(userId) + .eventId(eventId) + .storeName(storeName) + .storeCategory(storeCategory) + .storeDescription(storeDescription) + .objective(objective) .status("PENDING") .createdAt(LocalDateTime.now()) .build(); diff --git a/event-service/src/main/java/com/kt/event/eventservice/infrastructure/kafka/ImageJobKafkaProducer.java b/event-service/src/main/java/com/kt/event/eventservice/infrastructure/kafka/ImageJobKafkaProducer.java index 94dbbc5..1768c08 100644 --- a/event-service/src/main/java/com/kt/event/eventservice/infrastructure/kafka/ImageJobKafkaProducer.java +++ b/event-service/src/main/java/com/kt/event/eventservice/infrastructure/kafka/ImageJobKafkaProducer.java @@ -35,9 +35,9 @@ public class ImageJobKafkaProducer { /** * 이미지 생성 작업 메시지 발행 * - * @param jobId 작업 ID (UUID) - * @param userId 사용자 ID (UUID) - * @param eventId 이벤트 ID (UUID) + * @param jobId 작업 ID (JOB-{type}-{timestamp}-{random8}) + * @param userId 사용자 ID + * @param eventId 이벤트 ID (EVT-{storeId}-{yyyyMMddHHmmss}-{random8}) * @param prompt 이미지 생성 프롬프트 */ public void publishImageGenerationJob(