From c5f144803309149227bde4ce13b602e75fdd3a14 Mon Sep 17 00:00:00 2001 From: HUIJAEKO Date: Tue, 3 Mar 2026 18:01:13 +0900 Subject: [PATCH 1/2] =?UTF-8?q?fix:=20=EB=A7=A4=EB=AC=BC=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EB=B9=84=EB=8F=99=EA=B8=B0=20=EC=B2=98=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 + docker-compose.local.yml | 13 ++++ .../Dingle/global/config/RabbitConfig.java | 61 ++++++++++++++++++ .../controller/PropertyController.java | 3 + .../property/dto/PropertyCreatedEvent.java | 16 +++++ .../service/PropertyAsyncOrchestrator.java | 62 +++++++++++++++++++ .../property/service/PropertyService.java | 10 ++- .../util/PropertyCreatedConsumer.java | 19 ++++++ .../property/util/PropertyEventRelay.java | 25 ++++++++ 9 files changed, 210 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/Dingle/global/config/RabbitConfig.java create mode 100644 src/main/java/com/example/Dingle/property/dto/PropertyCreatedEvent.java create mode 100644 src/main/java/com/example/Dingle/property/service/PropertyAsyncOrchestrator.java create mode 100644 src/main/java/com/example/Dingle/property/util/PropertyCreatedConsumer.java create mode 100644 src/main/java/com/example/Dingle/property/util/PropertyEventRelay.java 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..05d1b26 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 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 + ); + } +} From eb76bd97d22e8c53e77ae642fec92eff5ed50901 Mon Sep 17 00:00:00 2001 From: HUIJAEKO Date: Tue, 3 Mar 2026 18:14:14 +0900 Subject: [PATCH 2/2] =?UTF-8?q?chore:=20docker-compose.local.yml=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docker-compose.local.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/docker-compose.local.yml b/docker-compose.local.yml index 05d1b26..f64d113 100644 --- a/docker-compose.local.yml +++ b/docker-compose.local.yml @@ -53,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