recommend
This commit is contained in:
+127
-5
@@ -1,10 +1,132 @@
|
||||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.4.0'
|
||||
id 'io.spring.dependency-management' version '1.1.6'
|
||||
}
|
||||
|
||||
group = 'com.ktds.hi'
|
||||
version = '1.0.0'
|
||||
|
||||
java {
|
||||
sourceCompatibility = '21'
|
||||
}
|
||||
|
||||
configurations {
|
||||
compileOnly {
|
||||
extendsFrom annotationProcessor
|
||||
}
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
// 공통 모듈
|
||||
implementation project(':common')
|
||||
|
||||
// AI and Location Services
|
||||
|
||||
// Spring Boot Starters
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-validation'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
implementation 'org.springframework.boot:spring-boot-starter-actuator'
|
||||
|
||||
// Database
|
||||
runtimeOnly 'org.postgresql:postgresql'
|
||||
|
||||
// Redis
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
|
||||
|
||||
// JSON Processing
|
||||
implementation 'com.fasterxml.jackson.core:jackson-databind'
|
||||
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310'
|
||||
|
||||
// Lombok
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
// OpenAPI/Swagger
|
||||
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
|
||||
|
||||
// Logging
|
||||
implementation 'org.springframework.boot:spring-boot-starter-logging'
|
||||
|
||||
// Security (JWT 처리용)
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'
|
||||
|
||||
// Apache Commons (유틸리티)
|
||||
implementation 'org.apache.commons:commons-lang3:3.13.0'
|
||||
implementation 'org.apache.commons:commons-collections4:4.4'
|
||||
|
||||
// AI 서비스 연동을 위한 HTTP 클라이언트
|
||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
||||
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'
|
||||
implementation 'org.springframework.cloud:spring-cloud-starter-loadbalancer'
|
||||
// 캐싱
|
||||
implementation 'org.springframework.boot:spring-boot-starter-cache'
|
||||
implementation 'com.github.ben-manes.caffeine:caffeine'
|
||||
|
||||
// 모니터링
|
||||
implementation 'io.micrometer:micrometer-registry-prometheus'
|
||||
|
||||
// 테스트
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
testImplementation 'org.testcontainers:junit-jupiter'
|
||||
testImplementation 'org.testcontainers:postgresql'
|
||||
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
|
||||
}
|
||||
|
||||
dependencyManagement {
|
||||
imports {
|
||||
mavenBom "org.testcontainers:testcontainers-bom:1.19.3"
|
||||
}
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
|
||||
// 테스트 환경 설정
|
||||
systemProperty 'spring.profiles.active', 'test'
|
||||
|
||||
// 테스트 시 메모리 설정
|
||||
minHeapSize = "512m"
|
||||
maxHeapSize = "1024m"
|
||||
}
|
||||
|
||||
// JAR 파일 이름 설정
|
||||
jar {
|
||||
archiveBaseName = 'recommend-service'
|
||||
archiveVersion = '1.0.0'
|
||||
enabled = false
|
||||
}
|
||||
|
||||
bootJar {
|
||||
archiveBaseName = 'recommend-service'
|
||||
archiveVersion = '1.0.0'
|
||||
archiveClassifier = ''
|
||||
}
|
||||
|
||||
// 컴파일 옵션
|
||||
compileJava {
|
||||
options.encoding = 'UTF-8'
|
||||
options.compilerArgs += ['-parameters']
|
||||
}
|
||||
|
||||
compileTestJava {
|
||||
options.encoding = 'UTF-8'
|
||||
}
|
||||
|
||||
// 리소스 인코딩
|
||||
processResources {
|
||||
filesMatching('**/*.properties') {
|
||||
filteringCharset = 'UTF-8'
|
||||
}
|
||||
filesMatching('**/*.yml') {
|
||||
filteringCharset = 'UTF-8'
|
||||
}
|
||||
}
|
||||
@@ -2,19 +2,22 @@ package com.ktds.hi.recommend;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
/**
|
||||
* 추천 서비스 메인 애플리케이션
|
||||
* 추천 서비스 메인 애플리케이션 클래스
|
||||
* 가게 추천, 취향 분석 기능을 제공
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = {
|
||||
"com.ktds.hi.recommend",
|
||||
"com.ktds.hi.common"
|
||||
})
|
||||
@EnableFeignClients
|
||||
@EnableJpaAuditing
|
||||
public class RecommendServiceApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(RecommendServiceApplication.class, args);
|
||||
}
|
||||
|
||||
+80
-4
@@ -1,4 +1,80 @@
|
||||
package com.ktds.hi.recommend.infra.config;
|
||||
|
||||
public class RecommendWebClientConfig {
|
||||
}
|
||||
package com.ktds.hi.recommend.infra.config;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import org.springframework.web.client.RestTemplate;
|
||||
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* 추천 서비스 WebClient 설정 클래스
|
||||
* 외부 서비스와의 HTTP 통신을 위한 설정
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Configuration
|
||||
public class RecommendWebClientConfig {
|
||||
|
||||
@Value("${services.store.url:http://store-service:8082}")
|
||||
private String storeServiceUrl;
|
||||
|
||||
@Value("${services.review.url:http://review-service:8083}")
|
||||
private String reviewServiceUrl;
|
||||
|
||||
@Value("${services.member.url:http://member-service:8081}")
|
||||
private String memberServiceUrl;
|
||||
|
||||
/**
|
||||
* 매장 서비스와 통신하기 위한 WebClient
|
||||
*/
|
||||
@Bean("storeWebClient")
|
||||
public WebClient storeWebClient() {
|
||||
return WebClient.builder()
|
||||
.baseUrl(storeServiceUrl)
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 리뷰 서비스와 통신하기 위한 WebClient
|
||||
*/
|
||||
@Bean("reviewWebClient")
|
||||
public WebClient reviewWebClient() {
|
||||
return WebClient.builder()
|
||||
.baseUrl(reviewServiceUrl)
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원 서비스와 통신하기 위한 WebClient
|
||||
*/
|
||||
@Bean("memberWebClient")
|
||||
public WebClient memberWebClient() {
|
||||
return WebClient.builder()
|
||||
.baseUrl(memberServiceUrl)
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 일반적인 외부 API 호출을 위한 WebClient
|
||||
*/
|
||||
@Bean("externalWebClient")
|
||||
public WebClient externalWebClient() {
|
||||
return WebClient.builder()
|
||||
.codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(2 * 1024 * 1024))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 호환성을 위한 RestTemplate (레거시 지원)
|
||||
*/
|
||||
@Bean
|
||||
public RestTemplate restTemplate() {
|
||||
return new RestTemplate();
|
||||
}
|
||||
}
|
||||
+150
-4
@@ -1,4 +1,150 @@
|
||||
package com.ktds.hi.recommend.infra.gateway;
|
||||
|
||||
public class ExternalServiceClient {
|
||||
}
|
||||
package com.ktds.hi.recommend.infra.gateway;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Qualifier;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.web.reactive.function.client.WebClient;
|
||||
import reactor.core.publisher.Mono;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 외부 서비스 클라이언트
|
||||
* 다른 마이크로서비스와의 HTTP 통신을 담당
|
||||
*
|
||||
* @author 하이오더 개발팀
|
||||
* @version 1.0.0
|
||||
*/
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
@Slf4j
|
||||
public class ExternalServiceClient {
|
||||
|
||||
@Qualifier("storeWebClient")
|
||||
private final WebClient storeWebClient;
|
||||
|
||||
@Qualifier("reviewWebClient")
|
||||
private final WebClient reviewWebClient;
|
||||
|
||||
@Qualifier("memberWebClient")
|
||||
private final WebClient memberWebClient;
|
||||
|
||||
/**
|
||||
* 매장 정보 조회
|
||||
*/
|
||||
public Mono<Map<String, Object>> getStoreInfo(Long storeId) {
|
||||
return storeWebClient
|
||||
.get()
|
||||
.uri("/api/stores/{storeId}", storeId)
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.doOnError(error -> log.error("매장 정보 조회 실패: storeId={}, error={}", storeId, error.getMessage()))
|
||||
.onErrorReturn(Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 매장 목록 조회 (위치 기반)
|
||||
*/
|
||||
public Mono<List<Map<String, Object>>> getStoresByLocation(Double latitude, Double longitude, Integer radius) {
|
||||
return storeWebClient
|
||||
.get()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/api/stores/search")
|
||||
.queryParam("latitude", latitude)
|
||||
.queryParam("longitude", longitude)
|
||||
.queryParam("radius", radius)
|
||||
.build())
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {})
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.doOnError(error -> log.error("위치 기반 매장 조회 실패: lat={}, lng={}, radius={}, error={}",
|
||||
latitude, longitude, radius, error.getMessage()))
|
||||
.onErrorReturn(List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 매장별 리뷰 조회
|
||||
*/
|
||||
public Mono<List<Map<String, Object>>> getStoreReviews(Long storeId, Integer limit) {
|
||||
return reviewWebClient
|
||||
.get()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/api/reviews/store/{storeId}")
|
||||
.queryParam("limit", limit)
|
||||
.build(storeId))
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {})
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.doOnError(error -> log.error("매장 리뷰 조회 실패: storeId={}, error={}", storeId, error.getMessage()))
|
||||
.onErrorReturn(List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원 취향 정보 조회
|
||||
*/
|
||||
public Mono<Map<String, Object>> getMemberPreferences(Long memberId) {
|
||||
return memberWebClient
|
||||
.get()
|
||||
.uri("/api/members/{memberId}/preferences", memberId)
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.doOnError(error -> log.error("회원 취향 정보 조회 실패: memberId={}, error={}", memberId, error.getMessage()))
|
||||
.onErrorReturn(Map.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 회원 주문 이력 조회
|
||||
*/
|
||||
public Mono<List<Map<String, Object>>> getMemberOrderHistory(Long memberId, Integer limit) {
|
||||
return memberWebClient
|
||||
.get()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/api/members/{memberId}/orders")
|
||||
.queryParam("limit", limit)
|
||||
.build(memberId))
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {})
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.doOnError(error -> log.error("회원 주문 이력 조회 실패: memberId={}, error={}", memberId, error.getMessage()))
|
||||
.onErrorReturn(List.of());
|
||||
}
|
||||
|
||||
/**
|
||||
* 매장 평점 및 통계 조회
|
||||
*/
|
||||
public Mono<Map<String, Object>> getStoreStatistics(Long storeId) {
|
||||
return storeWebClient
|
||||
.get()
|
||||
.uri("/api/stores/{storeId}/statistics", storeId)
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<Map<String, Object>>() {})
|
||||
.timeout(Duration.ofSeconds(5))
|
||||
.doOnError(error -> log.error("매장 통계 조회 실패: storeId={}, error={}", storeId, error.getMessage()))
|
||||
.onErrorReturn(Map.of("rating", 0.0, "reviewCount", 0));
|
||||
}
|
||||
|
||||
/**
|
||||
* 인기 매장 조회
|
||||
*/
|
||||
public Mono<List<Map<String, Object>>> getPopularStores(String category, Integer limit) {
|
||||
return storeWebClient
|
||||
.get()
|
||||
.uri(uriBuilder -> uriBuilder
|
||||
.path("/api/stores/popular")
|
||||
.queryParam("category", category)
|
||||
.queryParam("limit", limit)
|
||||
.build())
|
||||
.retrieve()
|
||||
.bodyToMono(new ParameterizedTypeReference<List<Map<String, Object>>>() {})
|
||||
.timeout(Duration.ofSeconds(10))
|
||||
.doOnError(error -> log.error("인기 매장 조회 실패: category={}, limit={}, error={}",
|
||||
category, limit, error.getMessage()))
|
||||
.onErrorReturn(List.of());
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user