diff --git a/build.gradle b/build.gradle index b755455..45201f8 100644 --- a/build.gradle +++ b/build.gradle @@ -60,6 +60,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-webflux' implementation "org.hibernate.orm:hibernate-spatial:6.6.39.Final" + implementation 'org.springframework.boot:spring-boot-starter-amqp' + } tasks.withType(JavaCompile) { diff --git a/docker-compose.local.yml b/docker-compose.local.yml index a89e117..f64d113 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -25,6 +25,19 @@ services: volumes: - /var/run/docker.sock:/var/run/docker.sock:ro + rabbitmq: + image: rabbitmq:3-management + container_name: rabbitmq + ports: + - "5672:5672" # AMQP + - "15672:15672" # Management UI + environment: + RABBITMQ_DEFAULT_USER: guest + RABBITMQ_DEFAULT_PASS: guest + volumes: + - rabbitmq-data:/var/lib/rabbitmq + restart: unless-stopped + app: build: . image: dingle-app @@ -40,3 +53,6 @@ services: SPRING_DATASOURCE_URL: jdbc:postgresql://${POSTGRES_URL}:5432/${POSTGRES_DB} SPRING_DATASOURCE_USERNAME: ${POSTGRES_USER} SPRING_DATASOURCE_PASSWORD: ${POSTGRES_PASSWORD} + +volumes: + rabbitmq-data: \ No newline at end of file diff --git a/src/main/java/com/example/Dingle/global/config/RabbitConfig.java b/src/main/java/com/example/Dingle/global/config/RabbitConfig.java new file mode 100644 index 0000000..feeca12 --- /dev/null +++ b/src/main/java/com/example/Dingle/global/config/RabbitConfig.java @@ -0,0 +1,61 @@ +package com.example.Dingle.global.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.amqp.core.*; +import org.springframework.amqp.rabbit.annotation.EnableRabbit; +import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory; +import org.springframework.amqp.rabbit.connection.ConnectionFactory; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + + +@Configuration +@EnableRabbit +public class RabbitConfig { + + public static final String EXCHANGE = "property.exchange"; + public static final String QUEUE = "property.created.queue"; + public static final String ROUTING_KEY = "property.created"; + + @Bean + public DirectExchange propertyExchange() { + return new DirectExchange(EXCHANGE); + } + + @Bean + public Queue propertyCreatedQueue() { + return QueueBuilder.durable(QUEUE).build(); + } + + @Bean + public Binding propertyCreatedBinding(Queue propertyCreatedQueue, DirectExchange propertyExchange) { + return BindingBuilder.bind(propertyCreatedQueue) + .to(propertyExchange) + .with(ROUTING_KEY); + } + + @Bean + public Jackson2JsonMessageConverter jackson2JsonMessageConverter(ObjectMapper objectMapper) { + return new Jackson2JsonMessageConverter(objectMapper); + } + + @Bean + public RabbitTemplate rabbitTemplate(ConnectionFactory cf, Jackson2JsonMessageConverter converter) { + RabbitTemplate template = new RabbitTemplate(cf); + template.setMessageConverter(converter); + return template; + } + + @Bean + public SimpleRabbitListenerContainerFactory rabbitListenerContainerFactory( + ConnectionFactory cf, + Jackson2JsonMessageConverter converter + ) { + SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory(); + factory.setConnectionFactory(cf); + factory.setMessageConverter(converter); + return factory; + } +} diff --git a/src/main/java/com/example/Dingle/property/controller/PropertyController.java b/src/main/java/com/example/Dingle/property/controller/PropertyController.java index e3c3d11..e9cd2eb 100644 --- a/src/main/java/com/example/Dingle/property/controller/PropertyController.java +++ b/src/main/java/com/example/Dingle/property/controller/PropertyController.java @@ -2,6 +2,7 @@ import com.example.Dingle.global.dto.ResponseDTO; import com.example.Dingle.property.dto.PropertyCompareDTO; +import com.example.Dingle.property.dto.PropertyCreatedEvent; import com.example.Dingle.property.dto.PropertyListDTO; import com.example.Dingle.property.dto.PropertyRegisterRequestDTO; import com.example.Dingle.property.service.PropertyListService; @@ -9,10 +10,12 @@ import com.example.Dingle.user.dto.CustomUserDetails; import io.swagger.v3.oas.annotations.Operation; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.http.ResponseEntity; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.*; +import java.time.LocalDate; import java.util.List; @RestController diff --git a/src/main/java/com/example/Dingle/property/dto/PropertyCreatedEvent.java b/src/main/java/com/example/Dingle/property/dto/PropertyCreatedEvent.java new file mode 100644 index 0000000..5207cb6 --- /dev/null +++ b/src/main/java/com/example/Dingle/property/dto/PropertyCreatedEvent.java @@ -0,0 +1,16 @@ +package com.example.Dingle.property.dto; + +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.time.LocalDate; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class PropertyCreatedEvent { + + private Long propertyId; + private LocalDate createdAt; +} diff --git a/src/main/java/com/example/Dingle/property/service/PropertyAsyncOrchestrator.java b/src/main/java/com/example/Dingle/property/service/PropertyAsyncOrchestrator.java new file mode 100644 index 0000000..d884a42 --- /dev/null +++ b/src/main/java/com/example/Dingle/property/service/PropertyAsyncOrchestrator.java @@ -0,0 +1,62 @@ +package com.example.Dingle.property.service; + +import com.example.Dingle.property.service.openAI.*; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.concurrent.Future; + +@Service +@RequiredArgsConstructor +public class PropertyAsyncOrchestrator { + private final EnvironmentExplanationService environmentExplanationService; + private final ConvenienceExplanationService convenienceExplanationService; + private final AccessibilityExplanationService accessibilityExplanationService; + private final NoiseExplanationService noiseExplanationService; + private final SafetyExplanationService safetyExplanationService; + + public void process(Long propertyId) { + ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor(); + + try { + Future f1 = executor.submit(() -> { + environmentExplanationService.evaluateAndDescribe(propertyId); + return null; + }); + + Future f2 = executor.submit(() -> { + convenienceExplanationService.evaluateAndDescribe(propertyId); + return null; + }); + + Future f3 = executor.submit(() -> { + accessibilityExplanationService.evaluateAndDescribe(propertyId); + return null; + }); + + Future f4 = executor.submit(() -> { + noiseExplanationService.evaluateAndDescribe(propertyId); + return null; + }); + + Future f5 = executor.submit(() -> { + safetyExplanationService.evaluateAndDescribe(propertyId); + return null; + }); + + f1.get(); + f2.get(); + f3.get(); + f4.get(); + f5.get(); + + } catch (Exception e) { + throw new RuntimeException("Property async processing failed", e); + + } finally { + executor.close(); + } + } +} diff --git a/src/main/java/com/example/Dingle/property/service/PropertyService.java b/src/main/java/com/example/Dingle/property/service/PropertyService.java index b3b8cf7..149a173 100644 --- a/src/main/java/com/example/Dingle/property/service/PropertyService.java +++ b/src/main/java/com/example/Dingle/property/service/PropertyService.java @@ -5,6 +5,7 @@ import com.example.Dingle.global.exception.BusinessException; import com.example.Dingle.global.message.BusinessErrorMessage; import com.example.Dingle.property.dto.DealRequestDTO; +import com.example.Dingle.property.dto.PropertyCreatedEvent; import com.example.Dingle.property.dto.PropertyRegisterRequestDTO; import com.example.Dingle.property.entity.*; import com.example.Dingle.property.repository.*; @@ -14,9 +15,11 @@ import com.example.Dingle.realtor.entity.Realtor; import com.example.Dingle.realtor.repository.RealtorRepository; import lombok.RequiredArgsConstructor; +import org.springframework.context.ApplicationEventPublisher; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDate; import java.util.List; @Service @@ -32,9 +35,10 @@ public class PropertyService { private final RealtorRepository realtorRepository; private final DistrictRepository districtRepository; private final PropertyImageRepository propertyImageRepository; + private final ApplicationEventPublisher applicationEventPublisher; @Transactional - public void register(PropertyRegisterRequestDTO request) { + public Long register(PropertyRegisterRequestDTO request) { Realtor realtor = realtorRepository.findById(1L) .orElseThrow(() -> new BusinessException(BusinessErrorMessage.REALTOR_NOT_EXISTS)); @@ -65,6 +69,10 @@ public void register(PropertyRegisterRequestDTO request) { saveOptions(property, request.getOptions()); saveFacilities(property, request.getFacilities()); saveImages(property, request); + + applicationEventPublisher.publishEvent(new PropertyCreatedEvent(property.getId(), LocalDate.now())); + + return property.getId(); } private void saveDeal(Property property, DealRequestDTO deal) { diff --git a/src/main/java/com/example/Dingle/property/util/PropertyCreatedConsumer.java b/src/main/java/com/example/Dingle/property/util/PropertyCreatedConsumer.java new file mode 100644 index 0000000..2f33398 --- /dev/null +++ b/src/main/java/com/example/Dingle/property/util/PropertyCreatedConsumer.java @@ -0,0 +1,19 @@ +package com.example.Dingle.property.util; + +import com.example.Dingle.global.config.RabbitConfig; +import com.example.Dingle.property.dto.PropertyCreatedEvent; +import com.example.Dingle.property.service.PropertyAsyncOrchestrator; +import lombok.RequiredArgsConstructor; +import org.springframework.amqp.rabbit.annotation.RabbitListener; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class PropertyCreatedConsumer { + private final PropertyAsyncOrchestrator orchestrator; + + @RabbitListener(queues = RabbitConfig.QUEUE) + public void handle(PropertyCreatedEvent event) { + orchestrator.process(event.getPropertyId()); + } +} diff --git a/src/main/java/com/example/Dingle/property/util/PropertyEventRelay.java b/src/main/java/com/example/Dingle/property/util/PropertyEventRelay.java new file mode 100644 index 0000000..2206eab --- /dev/null +++ b/src/main/java/com/example/Dingle/property/util/PropertyEventRelay.java @@ -0,0 +1,25 @@ +package com.example.Dingle.property.util; + +import com.example.Dingle.global.config.RabbitConfig; +import com.example.Dingle.property.dto.PropertyCreatedEvent; +import lombok.RequiredArgsConstructor; +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Component; +import org.springframework.transaction.event.TransactionPhase; +import org.springframework.transaction.event.TransactionalEventListener; + +@Component +@RequiredArgsConstructor +public class PropertyEventRelay { + + private final RabbitTemplate rabbitTemplate; + + @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT) + public void on(PropertyCreatedEvent event) { + rabbitTemplate.convertAndSend( + RabbitConfig.EXCHANGE, + RabbitConfig.ROUTING_KEY, + event + ); + } +}