diff --git a/build.gradle b/build.gradle index d2b05bd..053c33c 100644 --- a/build.gradle +++ b/build.gradle @@ -29,6 +29,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.kafka:spring-kafka' + testImplementation 'org.springframework.kafka:spring-kafka-test' compileOnly 'org.projectlombok:lombok' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' @@ -40,6 +42,9 @@ dependencies { implementation 'io.jsonwebtoken:jjwt-api:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' + + // .env 파일 로딩 + implementation 'me.paulschwarz:spring-dotenv:4.0.0' } tasks.named('test') { diff --git a/src/main/java/com/capstone/kkumteul/domain/fairytale/exception/FairytaleErrorCode.java b/src/main/java/com/capstone/kkumteul/domain/fairytale/exception/FairytaleErrorCode.java index ad82647..4c3abb1 100644 --- a/src/main/java/com/capstone/kkumteul/domain/fairytale/exception/FairytaleErrorCode.java +++ b/src/main/java/com/capstone/kkumteul/domain/fairytale/exception/FairytaleErrorCode.java @@ -4,7 +4,7 @@ import lombok.AllArgsConstructor; import lombok.Getter; -import static com.capstone.kkumteul.global.constant.StaticValue.*; +import static com.capstone.kkumteul.global.constant.StaticValue.NOT_FOUND; @Getter @AllArgsConstructor diff --git a/src/main/java/com/capstone/kkumteul/domain/fairytale/web/controller/FairytaleController.java b/src/main/java/com/capstone/kkumteul/domain/fairytale/web/controller/FairytaleController.java index 8780372..acc463a 100644 --- a/src/main/java/com/capstone/kkumteul/domain/fairytale/web/controller/FairytaleController.java +++ b/src/main/java/com/capstone/kkumteul/domain/fairytale/web/controller/FairytaleController.java @@ -3,10 +3,13 @@ import com.capstone.kkumteul.domain.fairytale.entity.Island; import com.capstone.kkumteul.domain.fairytale.service.FairytaleService; import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleDetailRes; +import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleGenerateReq; import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleListRes; +import com.capstone.kkumteul.domain.kafka.service.EventService; import com.capstone.kkumteul.domain.user.entity.User; import com.capstone.kkumteul.global.response.SuccessResponse; import com.capstone.kkumteul.global.security.AuthUser; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; @@ -21,6 +24,19 @@ public class FairytaleController { private final FairytaleService fairytaleService; + private final EventService eventService; + + @PostMapping + public ResponseEntity> createFairytale( + @AuthUser User user, + @Valid @RequestBody FairytaleGenerateReq request + ) { + eventService.createFairytaleMessageSend(user.getId(), request); + + return ResponseEntity + .status(HttpStatus.CREATED) + .body(SuccessResponse.empty()); + } @GetMapping("/my") public ResponseEntity>> getMyFairytales( diff --git a/src/main/java/com/capstone/kkumteul/domain/fairytale/web/dto/FairytaleGenerateReq.java b/src/main/java/com/capstone/kkumteul/domain/fairytale/web/dto/FairytaleGenerateReq.java new file mode 100644 index 0000000..386732a --- /dev/null +++ b/src/main/java/com/capstone/kkumteul/domain/fairytale/web/dto/FairytaleGenerateReq.java @@ -0,0 +1,23 @@ +package com.capstone.kkumteul.domain.fairytale.web.dto; + +import com.capstone.kkumteul.domain.fairytale.entity.Background; +import com.capstone.kkumteul.domain.fairytale.entity.CharSpecies; +import com.capstone.kkumteul.domain.fairytale.entity.Morality; +import jakarta.validation.constraints.NotNull; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public class FairytaleGenerateReq { + + @NotNull(message = "배경은 비어있을 수 없습니다.") + private final Background background; + + @NotNull(message = "등장인물 종류는 비어있을 수 없습니다.") + private final CharSpecies charSpecies; + + @NotNull(message = "교훈은 비어있을 수 없습니다.") + private final Morality morality; + +} diff --git a/src/main/java/com/capstone/kkumteul/domain/game/entity/NodeCategory.java b/src/main/java/com/capstone/kkumteul/domain/game/entity/NodeCategory.java index cd29c58..3cf97a6 100644 --- a/src/main/java/com/capstone/kkumteul/domain/game/entity/NodeCategory.java +++ b/src/main/java/com/capstone/kkumteul/domain/game/entity/NodeCategory.java @@ -1,10 +1,9 @@ package com.capstone.kkumteul.domain.game.entity; +import com.capstone.kkumteul.domain.game.exception.InvalidCategoryException; import lombok.AllArgsConstructor; import lombok.Getter; -import com.capstone.kkumteul.domain.game.exception.InvalidCategoryException; - import java.util.Arrays; @Getter diff --git a/src/main/java/com/capstone/kkumteul/domain/game/service/GameServiceImpl.java b/src/main/java/com/capstone/kkumteul/domain/game/service/GameServiceImpl.java index 68fe4c7..6ea1cb9 100644 --- a/src/main/java/com/capstone/kkumteul/domain/game/service/GameServiceImpl.java +++ b/src/main/java/com/capstone/kkumteul/domain/game/service/GameServiceImpl.java @@ -1,12 +1,8 @@ package com.capstone.kkumteul.domain.game.service; import com.capstone.kkumteul.domain.fairytale.entity.Fairytale; -import com.capstone.kkumteul.domain.game.entity.GraphEdge; -import com.capstone.kkumteul.domain.game.entity.GraphNode; -import com.capstone.kkumteul.domain.game.entity.NodeCategory; -import com.capstone.kkumteul.domain.game.entity.EdgeChoice; -import com.capstone.kkumteul.domain.game.entity.GameResult; import com.capstone.kkumteul.domain.fairytale.exception.FairytaleNotFoundException; +import com.capstone.kkumteul.domain.game.entity.*; import com.capstone.kkumteul.domain.game.exception.*; import com.capstone.kkumteul.domain.game.repository.EdgeChoiceRepository; import com.capstone.kkumteul.domain.game.repository.GameResultRepository; diff --git a/src/main/java/com/capstone/kkumteul/domain/kafka/dto/FairytaleGenerateMessage.java b/src/main/java/com/capstone/kkumteul/domain/kafka/dto/FairytaleGenerateMessage.java new file mode 100644 index 0000000..f0488af --- /dev/null +++ b/src/main/java/com/capstone/kkumteul/domain/kafka/dto/FairytaleGenerateMessage.java @@ -0,0 +1,20 @@ +package com.capstone.kkumteul.domain.kafka.dto; + +import com.capstone.kkumteul.domain.fairytale.entity.Background; +import com.capstone.kkumteul.domain.fairytale.entity.CharSpecies; +import com.capstone.kkumteul.domain.fairytale.entity.Morality; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +@AllArgsConstructor +public class FairytaleGenerateMessage implements MessageInterface { + + private final Long userId; + + private final Background background; + private final CharSpecies charSpecies; + private final Morality morality; +} diff --git a/src/main/java/com/capstone/kkumteul/domain/kafka/dto/MessageInterface.java b/src/main/java/com/capstone/kkumteul/domain/kafka/dto/MessageInterface.java new file mode 100644 index 0000000..fd733ce --- /dev/null +++ b/src/main/java/com/capstone/kkumteul/domain/kafka/dto/MessageInterface.java @@ -0,0 +1,6 @@ +package com.capstone.kkumteul.domain.kafka.dto; + +public interface MessageInterface { + + Long getUserId(); +} diff --git a/src/main/java/com/capstone/kkumteul/domain/kafka/service/EventService.java b/src/main/java/com/capstone/kkumteul/domain/kafka/service/EventService.java new file mode 100644 index 0000000..18d4aeb --- /dev/null +++ b/src/main/java/com/capstone/kkumteul/domain/kafka/service/EventService.java @@ -0,0 +1,43 @@ +package com.capstone.kkumteul.domain.kafka.service; + +import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleGenerateReq; +import com.capstone.kkumteul.domain.kafka.dto.FairytaleGenerateMessage; +import com.capstone.kkumteul.domain.kafka.dto.MessageInterface; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.stereotype.Service; + +/* 동화 생성 이벤트 전파 */ + +@Service +@Slf4j(topic = "event") +@RequiredArgsConstructor +public class EventService { + + private final KafkaTemplate kafkaTemplate; + + @Value("${FAIRYTALE_GENERATION}") + private String FAIRYTALE_GENERATION; + + public void createFairytaleMessageSend(Long userId, FairytaleGenerateReq request) { + + FairytaleGenerateMessage message = FairytaleGenerateMessage.builder() + .userId(userId) + .background(request.getBackground()) + .charSpecies(request.getCharSpecie()) + .morality(request.getMorality()) + .build(); + + log.info("fairytale_generate userId={}, message={}", userId, message); + + kafkaTemplate.send(FAIRYTALE_GENERATION, message) + .whenComplete((result, e) -> { + if (e != null) { + log.error("fairytale_generate failed", e); + } + }); + + } +} diff --git a/src/main/java/com/capstone/kkumteul/global/config/KafkaProducerConfig.java b/src/main/java/com/capstone/kkumteul/global/config/KafkaProducerConfig.java new file mode 100644 index 0000000..f04bf37 --- /dev/null +++ b/src/main/java/com/capstone/kkumteul/global/config/KafkaProducerConfig.java @@ -0,0 +1,46 @@ +package com.capstone.kkumteul.global.config; + +import com.capstone.kkumteul.domain.kafka.dto.MessageInterface; +import org.apache.kafka.clients.producer.ProducerConfig; +import org.apache.kafka.common.serialization.StringSerializer; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.kafka.core.DefaultKafkaProducerFactory; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.core.ProducerFactory; +import org.springframework.kafka.support.serializer.JsonSerializer; + +import java.util.HashMap; +import java.util.Map; + +@Configuration +public class KafkaProducerConfig { + + private final String KAFKA_URL; + + public KafkaProducerConfig( + @Value("${KAFKA_URL}") String KAFKA_URL + ) { this.KAFKA_URL = KAFKA_URL; } + + @Bean + + public ProducerFactory producerFactory() { + + Map config = new HashMap<>(); + + // key - String Serialization + // value - Json Serialization + config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_URL); + config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class); + config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class); + config.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false); + + return new DefaultKafkaProducerFactory<>(config); + } + + @Bean + public KafkaTemplate kafkaTemplate() { + return new KafkaTemplate<>(producerFactory()); + } +} diff --git a/src/main/java/com/capstone/kkumteul/global/exception/GlobalExceptionHandler.java b/src/main/java/com/capstone/kkumteul/global/exception/GlobalExceptionHandler.java index d35e58e..477184d 100644 --- a/src/main/java/com/capstone/kkumteul/global/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/capstone/kkumteul/global/exception/GlobalExceptionHandler.java @@ -8,10 +8,10 @@ import org.springframework.validation.BindException; import org.springframework.web.HttpRequestMethodNotSupportedException; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException; -import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.multipart.support.MissingServletRequestPartException; import org.springframework.web.servlet.NoHandlerFoundException; import org.springframework.web.servlet.resource.NoResourceFoundException; diff --git a/src/test/java/com/capstone/kkumteul/kafka/KafkaProducerTest.java b/src/test/java/com/capstone/kkumteul/kafka/KafkaProducerTest.java new file mode 100644 index 0000000..9578cc9 --- /dev/null +++ b/src/test/java/com/capstone/kkumteul/kafka/KafkaProducerTest.java @@ -0,0 +1,52 @@ +package com.capstone.kkumteul.kafka; + +import com.capstone.kkumteul.domain.fairytale.entity.Background; +import com.capstone.kkumteul.domain.fairytale.entity.CharSpecies; +import com.capstone.kkumteul.domain.fairytale.entity.Morality; +import com.capstone.kkumteul.domain.kafka.dto.FairytaleGenerateMessage; +import com.capstone.kkumteul.domain.kafka.dto.MessageInterface; +import com.capstone.kkumteul.global.config.KafkaProducerConfig; +import lombok.extern.slf4j.Slf4j; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.kafka.core.KafkaTemplate; +import org.springframework.kafka.support.SendResult; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +@Slf4j +@SpringBootTest( + classes = KafkaProducerConfig.class +) +class KafkaProducerTest { + + @Autowired + private KafkaTemplate kafkaTemplate; + + @Test + void messagePublicationTest() throws Exception { + + FairytaleGenerateMessage message = FairytaleGenerateMessage.builder() + .userId(1L) + .background(Background.FOREST_NATURE) + .charSpecies(CharSpecies.ANIMAL) + .morality(Morality.KINDNESS_REWARDED) + .build(); + + CompletableFuture> future = + kafkaTemplate.send("fairytale_generate", message); + + SendResult result = future.get(10, TimeUnit.SECONDS); + + assertThat(result.getRecordMetadata().topic()).isEqualTo("fairytale_generate"); + assertThat(result.getRecordMetadata().offset()).isGreaterThanOrEqualTo(0); + + log.info("토픽: {}", result.getRecordMetadata().topic()); + log.info("파티션: {}", result.getRecordMetadata().partition()); + log.info("오프셋: {}", result.getRecordMetadata().offset()); + } +}