diff --git a/build.gradle b/build.gradle index 852bba6..967144f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,13 +1,13 @@ plugins { id 'java' id 'org.springframework.boot' version '3.4.0' apply false - id 'io.spring.dependency-management' version '1.1.4' apply false + id 'io.spring.dependency-management' version '1.1.6' apply false } allprojects { group = 'com.ktds.hi' version = '1.0.0' - + repositories { mavenCentral() } @@ -17,49 +17,37 @@ subprojects { apply plugin: 'java' apply plugin: 'org.springframework.boot' apply plugin: 'io.spring.dependency-management' - + java { - toolchain { - languageVersion = JavaLanguageVersion.of(21) + sourceCompatibility = '21' + } + + configurations { + compileOnly { + extendsFrom annotationProcessor } } - + + // 공통 의존성 dependencies { - // 공통 의존성 - implementation 'org.springframework.boot:spring-boot-starter-web' - implementation 'org.springframework.boot:spring-boot-starter-data-jpa' - implementation 'org.springframework.boot:spring-boot-starter-validation' - implementation 'org.springframework.boot:spring-boot-starter-security' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' - implementation 'org.springframework.boot:spring-boot-starter-actuator' - - // Swagger - implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' - - // JWT - implementation 'io.jsonwebtoken:jjwt-api:0.12.3' - runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3' - runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3' - - // Database - runtimeOnly 'org.postgresql:postgresql' - - // Lombok + // Lombok (모든 서브 프로젝트에서 사용) compileOnly 'org.projectlombok:lombok' annotationProcessor 'org.projectlombok:lombok' - - // MapStruct - implementation 'org.mapstruct:mapstruct:1.5.5.Final' - annotationProcessor 'org.mapstruct:mapstruct-processor:1.5.5.Final' - - // Test + + // 테스트 (모든 서브 프로젝트에서 사용) 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' } - + + // 컴파일 옵션 + tasks.withType(JavaCompile) { + options.encoding = 'UTF-8' + options.compilerArgs += ['-parameters'] + } + + // 테스트 설정 tasks.named('test') { useJUnitPlatform() + systemProperty 'spring.profiles.active', 'test' } -} +} \ No newline at end of file diff --git a/common/src/build.gradle b/common/src/build.gradle index e69de29..714c145 100644 --- a/common/src/build.gradle +++ b/common/src/build.gradle @@ -0,0 +1,54 @@ +plugins { + id 'java-library' +} + +description = 'Common utilities and shared components' + +dependencies { + // Spring Boot Starters + api 'org.springframework.boot:spring-boot-starter-web' + api 'org.springframework.boot:spring-boot-starter-data-jpa' + api 'org.springframework.boot:spring-boot-starter-validation' + api 'org.springframework.boot:spring-boot-starter-security' + + // AOP (AspectJ) - 명시적 의존성 추가 + api 'org.springframework.boot:spring-boot-starter-aop' + api 'org.aspectj:aspectjweaver:1.9.21' + api 'org.aspectj:aspectjrt:1.9.21' + + // JSON Processing + api 'com.fasterxml.jackson.core:jackson-databind' + api 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310' + + // JWT + api '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 + api 'org.apache.commons:commons-lang3:3.13.0' + api 'org.apache.commons:commons-collections4:4.4' + + // OpenAPI/Swagger + api 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0' + + // Lombok + compileOnly 'org.projectlombok:lombok' + annotationProcessor 'org.projectlombok:lombok' + + // 테스트 + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'org.springframework.security:spring-security-test' +} + +// JAR 생성 설정 (plain jar도 생성) +jar { + archiveBaseName = 'common' + archiveVersion = '1.0.0' + enabled = true +} + +// bootJar 비활성화 (라이브러리이므로 실행 가능한 JAR 불필요) +bootJar { + enabled = false +} \ No newline at end of file diff --git a/common/src/main/java/com/ktds/hi/common/config/AsyncConfig.java b/common/src/main/java/com/ktds/hi/common/config/AsyncConfig.java index 96e51a6..b73aecf 100644 --- a/common/src/main/java/com/ktds/hi/common/config/AsyncConfig.java +++ b/common/src/main/java/com/ktds/hi/common/config/AsyncConfig.java @@ -1,4 +1,52 @@ -package com.ktds.hi.common.config; - -public class AsyncConfig { -} +package com.ktds.hi.common.config; + +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.scheduling.annotation.EnableAsync; +import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor; + +import java.util.concurrent.Executor; + +/** + * 비동기 처리 설정 클래스 + * @Async 어노테이션을 위한 스레드 풀 설정 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@Configuration +@EnableAsync +public class AsyncConfig { + + /** + * 감사 로그용 스레드 풀 + */ + @Bean("auditTaskExecutor") + public Executor auditTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(2); + executor.setMaxPoolSize(5); + executor.setQueueCapacity(100); + executor.setThreadNamePrefix("audit-"); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(30); + executor.initialize(); + return executor; + } + + /** + * 일반적인 비동기 작업용 스레드 풀 + */ + @Bean("generalTaskExecutor") + public Executor generalTaskExecutor() { + ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor(); + executor.setCorePoolSize(4); + executor.setMaxPoolSize(10); + executor.setQueueCapacity(200); + executor.setThreadNamePrefix("async-"); + executor.setWaitForTasksToCompleteOnShutdown(true); + executor.setAwaitTerminationSeconds(30); + executor.initialize(); + return executor; + } +} \ No newline at end of file diff --git a/common/src/main/java/com/ktds/hi/common/config/CommonAopConfig.java b/common/src/main/java/com/ktds/hi/common/config/CommonAopConfig.java index aff92ba..c90610f 100644 --- a/common/src/main/java/com/ktds/hi/common/config/CommonAopConfig.java +++ b/common/src/main/java/com/ktds/hi/common/config/CommonAopConfig.java @@ -1,4 +1,18 @@ -package com.ktds.hi.common.config; - -public class CommonAopConfig { -} +package com.ktds.hi.common.config; + +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.EnableAspectJAutoProxy; + +/** + * AOP 설정 클래스 + * AspectJ 자동 프록시를 활성화 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +@Configuration +@EnableAspectJAutoProxy +public class CommonAopConfig { + // AOP 자동 설정을 위한 설정 클래스 + // @EnableAspectJAutoProxy 어노테이션으로 AspectJ 자동 프록시 활성화 +} \ No newline at end of file diff --git a/common/src/main/java/com/ktds/hi/common/service/AuditAction.java b/common/src/main/java/com/ktds/hi/common/service/AuditAction.java index 7acd20d..8326f46 100644 --- a/common/src/main/java/com/ktds/hi/common/service/AuditAction.java +++ b/common/src/main/java/com/ktds/hi/common/service/AuditAction.java @@ -1,4 +1,76 @@ -package com.ktds.hi.common.service; - -public class AuditAction { -} +package com.ktds.hi.common.service; + +/** + * 감사 액션 열거형 + * 시스템에서 발생하는 주요 액션들을 정의 + * + * @author 하이오더 개발팀 + * @version 1.0.0 + */ +public enum AuditAction { + + /** + * 생성 액션 + */ + CREATE("생성"), + + /** + * 수정 액션 + */ + UPDATE("수정"), + + /** + * 삭제 액션 + */ + DELETE("삭제"), + + /** + * 조회 액션 + */ + ACCESS("조회"), + + /** + * 로그인 액션 + */ + LOGIN("로그인"), + + /** + * 로그아웃 액션 + */ + LOGOUT("로그아웃"), + + /** + * 승인 액션 + */ + APPROVE("승인"), + + /** + * 거부 액션 + */ + REJECT("거부"), + + /** + * 활성화 액션 + */ + ACTIVATE("활성화"), + + /** + * 비활성화 액션 + */ + DEACTIVATE("비활성화"); + + private final String description; + + AuditAction(String description) { + this.description = description; + } + + public String getDescription() { + return description; + } + + @Override + public String toString() { + return description; + } +} \ No newline at end of file diff --git a/common/src/main/java/com/ktds/hi/common/service/AuditLogService.java b/common/src/main/java/com/ktds/hi/common/service/AuditLogService.java index 8eb0666..4798a42 100644 --- a/common/src/main/java/com/ktds/hi/common/service/AuditLogService.java +++ b/common/src/main/java/com/ktds/hi/common/service/AuditLogService.java @@ -1,51 +1,41 @@ package com.ktds.hi.common.service; -import com.ktds.hi.common.audit.AuditAction; -import com.ktds.hi.common.audit.AuditLog; -import com.ktds.hi.common.audit.AuditLogger; -import com.ktds.hi.common.repository.AuditLogRepository; -import com.ktds.hi.common.security.SecurityUtil; -import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; /** * 감사 로그 서비스 - * 시스템의 중요한 액션들을 비동기적으로 로깅 + * 시스템 내 중요한 작업들을 로깅 + * + * @author 하이오더 개발팀 + * @version 1.0.0 */ @Service -@RequiredArgsConstructor @Slf4j public class AuditLogService { - private final AuditLogRepository auditLogRepository; - /** - * 감사 로그 기록 (비동기) + * 비동기 로그 기록 */ @Async - @Transactional public void logAsync(AuditAction action, String entityType, String entityId, String description) { - log(action, entityType, entityId, description); - } - - /** - * 감사 로그 기록 (동기) - */ - @Transactional - public void log(AuditAction action, String entityType, String entityId, String description) { try { - Long userId = SecurityUtil.getCurrentUserId().orElse(null); - String username = SecurityUtil.getCurrentUsername().orElse("SYSTEM"); + // 현재 사용자 정보 가져오기 (SecurityContext에서) + String userId = getCurrentUserId(); + String username = getCurrentUsername(); - AuditLog auditLog = AuditLogger.create(userId, username, action, entityType, entityId, description); - auditLogRepository.save(auditLog); + // 감사 로그 생성 및 저장 + log.info("AUDIT_LOG: action={}, entityType={}, entityId={}, userId={}, username={}, description={}", + action, entityType, entityId, userId, username, description); + + // 실제 환경에서는 데이터베이스에 저장 + // AuditLog auditLog = AuditLog.create(userId, username, action, entityType, entityId, description); + // auditLogRepository.save(auditLog); } catch (Exception e) { log.error("Failed to save audit log: action={}, entityType={}, entityId={}", - action, entityType, entityId, e); + action, entityType, entityId, e); } } @@ -81,13 +71,38 @@ public class AuditLogService { * 로그인 로그 */ public void logLogin(String description) { - logAsync(AuditAction.LOGIN, "USER", SecurityUtil.getCurrentUserId().map(String::valueOf).orElse("UNKNOWN"), description); + logAsync(AuditAction.LOGIN, "USER", getCurrentUserId(), description); } /** * 로그아웃 로그 */ public void logLogout(String description) { - logAsync(AuditAction.LOGOUT, "USER", SecurityUtil.getCurrentUserId().map(String::valueOf).orElse("UNKNOWN"), description); + logAsync(AuditAction.LOGOUT, "USER", getCurrentUserId(), description); } -} + + /** + * 현재 사용자 ID 조회 + */ + private String getCurrentUserId() { + try { + // SecurityContext에서 사용자 ID 추출 + // 실제 구현에서는 SecurityContextHolder 사용 + return "SYSTEM"; // 임시값 + } catch (Exception e) { + return "UNKNOWN"; + } + } + + /** + * 현재 사용자명 조회 + */ + private String getCurrentUsername() { + try { + // SecurityContext에서 사용자명 추출 + return "system"; // 임시값 + } catch (Exception e) { + return "unknown"; + } + } +} \ No newline at end of file diff --git a/recommend/build.gradle b/recommend/build.gradle index 6a199a9..18df623 100644 --- a/recommend/build.gradle +++ b/recommend/build.gradle @@ -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' + } +} \ No newline at end of file diff --git a/recommend/src/main/java/com/ktds/hi/recommend/RecommendServiceApplication.java b/recommend/src/main/java/com/ktds/hi/recommend/RecommendServiceApplication.java index be8b490..388cfb5 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/RecommendServiceApplication.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/RecommendServiceApplication.java @@ -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); } diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/config/RecommendWebClientConfig.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/config/RecommendWebClientConfig.java index 6029fd9..5d08ec9 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/config/RecommendWebClientConfig.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/config/RecommendWebClientConfig.java @@ -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(); + } +} \ No newline at end of file diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/gateway/ExternalServiceClient.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/gateway/ExternalServiceClient.java index 22a4427..b7ea723 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/gateway/ExternalServiceClient.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/gateway/ExternalServiceClient.java @@ -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> getStoreInfo(Long storeId) { + return storeWebClient + .get() + .uri("/api/stores/{storeId}", storeId) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .timeout(Duration.ofSeconds(5)) + .doOnError(error -> log.error("매장 정보 조회 실패: storeId={}, error={}", storeId, error.getMessage())) + .onErrorReturn(Map.of()); + } + + /** + * 매장 목록 조회 (위치 기반) + */ + public Mono>> 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>>() {}) + .timeout(Duration.ofSeconds(10)) + .doOnError(error -> log.error("위치 기반 매장 조회 실패: lat={}, lng={}, radius={}, error={}", + latitude, longitude, radius, error.getMessage())) + .onErrorReturn(List.of()); + } + + /** + * 매장별 리뷰 조회 + */ + public Mono>> 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>>() {}) + .timeout(Duration.ofSeconds(5)) + .doOnError(error -> log.error("매장 리뷰 조회 실패: storeId={}, error={}", storeId, error.getMessage())) + .onErrorReturn(List.of()); + } + + /** + * 회원 취향 정보 조회 + */ + public Mono> getMemberPreferences(Long memberId) { + return memberWebClient + .get() + .uri("/api/members/{memberId}/preferences", memberId) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .timeout(Duration.ofSeconds(5)) + .doOnError(error -> log.error("회원 취향 정보 조회 실패: memberId={}, error={}", memberId, error.getMessage())) + .onErrorReturn(Map.of()); + } + + /** + * 회원 주문 이력 조회 + */ + public Mono>> 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>>() {}) + .timeout(Duration.ofSeconds(5)) + .doOnError(error -> log.error("회원 주문 이력 조회 실패: memberId={}, error={}", memberId, error.getMessage())) + .onErrorReturn(List.of()); + } + + /** + * 매장 평점 및 통계 조회 + */ + public Mono> getStoreStatistics(Long storeId) { + return storeWebClient + .get() + .uri("/api/stores/{storeId}/statistics", storeId) + .retrieve() + .bodyToMono(new ParameterizedTypeReference>() {}) + .timeout(Duration.ofSeconds(5)) + .doOnError(error -> log.error("매장 통계 조회 실패: storeId={}, error={}", storeId, error.getMessage())) + .onErrorReturn(Map.of("rating", 0.0, "reviewCount", 0)); + } + + /** + * 인기 매장 조회 + */ + public Mono>> 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>>() {}) + .timeout(Duration.ofSeconds(10)) + .doOnError(error -> log.error("인기 매장 조회 실패: category={}, limit={}, error={}", + category, limit, error.getMessage())) + .onErrorReturn(List.of()); + } +} \ No newline at end of file