diff --git a/recommend/build.gradle b/recommend/build.gradle index 1fc1174..6a199a9 100644 --- a/recommend/build.gradle +++ b/recommend/build.gradle @@ -3,4 +3,8 @@ dependencies { // AI and Location Services 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' } 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 e6dfce8..be8b490 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/RecommendServiceApplication.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/RecommendServiceApplication.java @@ -2,20 +2,20 @@ 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"}) +@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); } -} +} \ No newline at end of file diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/controller/StoreRecommendController.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/controller/StoreRecommendController.java index 5d51609..3caa83a 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/controller/StoreRecommendController.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/controller/StoreRecommendController.java @@ -1,12 +1,18 @@ +// recommend/src/main/java/com/ktds/hi/recommend/infra/controller/StoreRecommendController.java package com.ktds.hi.recommend.infra.controller; import com.ktds.hi.recommend.biz.usecase.in.StoreRecommendUseCase; import com.ktds.hi.recommend.infra.dto.request.RecommendStoreRequest; import com.ktds.hi.recommend.infra.dto.response.RecommendStoreResponse; +import com.ktds.hi.recommend.infra.dto.response.StoreDetailResponse; +import com.ktds.hi.common.dto.response.ApiResponse; +import com.ktds.hi.common.dto.response.PageResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; @@ -14,55 +20,93 @@ import org.springframework.web.bind.annotation.*; import java.util.List; /** - * 매장 추천 컨트롤러 클래스 - * 매장 추천 관련 API를 제공 + * 매장 추천 컨트롤러 */ @RestController -@RequestMapping("/api/recommend") +@RequestMapping("/api/recommend/stores") @RequiredArgsConstructor @Tag(name = "매장 추천 API", description = "사용자 취향 기반 매장 추천 관련 API") public class StoreRecommendController { - + private final StoreRecommendUseCase storeRecommendUseCase; - + /** - * 사용자 취향 기반 매장 추천 API + * 개인화 매장 추천 */ - @PostMapping("/stores") - @Operation(summary = "매장 추천", description = "사용자 취향과 위치를 기반으로 매장을 추천합니다.") - public ResponseEntity> recommendStores(Authentication authentication, - @Valid @RequestBody RecommendStoreRequest request) { + @PostMapping + @Operation(summary = "개인화 매장 추천", description = "사용자 취향과 위치를 기반으로 매장을 추천합니다.") + public ResponseEntity>> recommendStores( + Authentication authentication, + @Valid @RequestBody RecommendStoreRequest request) { + Long memberId = Long.valueOf(authentication.getName()); - List recommendations = storeRecommendUseCase.recommendStores(memberId, request); - return ResponseEntity.ok(recommendations); + List recommendations = + storeRecommendUseCase.recommendPersonalizedStores(memberId, request); + + return ResponseEntity.ok(ApiResponse.success(recommendations)); } - + /** - * 위치 기반 매장 추천 API + * 위치 기반 매장 추천 */ - @GetMapping("/stores/nearby") + @GetMapping("/nearby") @Operation(summary = "주변 매장 추천", description = "현재 위치 기반으로 주변 매장을 추천합니다.") - public ResponseEntity> recommendNearbyStores( + public ResponseEntity>> recommendNearbyStores( @RequestParam Double latitude, @RequestParam Double longitude, - @RequestParam(defaultValue = "5000") Integer radius) { - - List recommendations = storeRecommendUseCase - .recommendStoresByLocation(latitude, longitude, radius); - return ResponseEntity.ok(recommendations); + @RequestParam(defaultValue = "5000") Integer radius, + @RequestParam(required = false) String category, + @PageableDefault(size = 20) Pageable pageable) { + + PageResponse recommendations = + storeRecommendUseCase.recommendStoresByLocation(latitude, longitude, radius, category, pageable); + + return ResponseEntity.ok(ApiResponse.success(recommendations)); } - + /** - * 인기 매장 추천 API + * 인기 매장 추천 */ - @GetMapping("/stores/popular") + @GetMapping("/popular") @Operation(summary = "인기 매장 추천", description = "카테고리별 인기 매장을 추천합니다.") - public ResponseEntity> recommendPopularStores( + public ResponseEntity>> recommendPopularStores( @RequestParam(required = false) String category, @RequestParam(defaultValue = "10") Integer limit) { - - List recommendations = storeRecommendUseCase - .recommendPopularStores(category, limit); - return ResponseEntity.ok(recommendations); + + List recommendations = + storeRecommendUseCase.recommendPopularStores(category, limit); + + return ResponseEntity.ok(ApiResponse.success(recommendations)); } -} + + /** + * 추천 매장 상세 조회 + */ + @GetMapping("/{storeId}") + @Operation(summary = "추천 매장 상세 조회", description = "추천된 매장의 상세 정보를 조회합니다.") + public ResponseEntity> getRecommendedStoreDetail( + @PathVariable Long storeId, + Authentication authentication) { + + Long memberId = authentication != null ? Long.valueOf(authentication.getName()) : null; + StoreDetailResponse storeDetail = + storeRecommendUseCase.getRecommendedStoreDetail(storeId, memberId); + + return ResponseEntity.ok(ApiResponse.success(storeDetail)); + } + + /** + * 추천 클릭 로깅 + */ + @PostMapping("/{storeId}/click") + @Operation(summary = "추천 클릭 로깅", description = "추천된 매장 클릭을 로깅합니다.") + public ResponseEntity> logRecommendClick( + @PathVariable Long storeId, + Authentication authentication) { + + Long memberId = Long.valueOf(authentication.getName()); + storeRecommendUseCase.logRecommendClick(memberId, storeId); + + return ResponseEntity.ok(ApiResponse.success()); + } +} \ No newline at end of file diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/controller/TasteAnalysisController.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/controller/TasteAnalysisController.java index 6c8a388..6798770 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/controller/TasteAnalysisController.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/controller/TasteAnalysisController.java @@ -1,46 +1,69 @@ package com.ktds.hi.recommend.infra.controller; import com.ktds.hi.recommend.biz.usecase.in.TasteAnalysisUseCase; +import com.ktds.hi.recommend.infra.dto.request.TasteUpdateRequest; import com.ktds.hi.recommend.infra.dto.response.TasteAnalysisResponse; -import com.ktds.hi.common.dto.SuccessResponse; +import com.ktds.hi.recommend.infra.dto.response.PreferenceTagResponse; +import com.ktds.hi.common.dto.response.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; +import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.http.ResponseEntity; import org.springframework.security.core.Authentication; import org.springframework.web.bind.annotation.*; +import java.util.List; + /** - * 취향 분석 컨트롤러 클래스 - * 사용자 취향 분석 관련 API를 제공 + * 취향 분석 컨트롤러 */ @RestController @RequestMapping("/api/recommend/taste") @RequiredArgsConstructor @Tag(name = "취향 분석 API", description = "사용자 취향 분석 관련 API") public class TasteAnalysisController { - + private final TasteAnalysisUseCase tasteAnalysisUseCase; - + /** - * 사용자 취향 분석 조회 API + * 사용자 취향 분석 조회 */ @GetMapping("/analysis") @Operation(summary = "취향 분석 조회", description = "현재 로그인한 사용자의 취향 분석 결과를 조회합니다.") - public ResponseEntity getMemberTasteAnalysis(Authentication authentication) { + public ResponseEntity> getMemberTasteAnalysis( + Authentication authentication) { + Long memberId = Long.valueOf(authentication.getName()); TasteAnalysisResponse analysis = tasteAnalysisUseCase.analyzeMemberTaste(memberId); - return ResponseEntity.ok(analysis); + + return ResponseEntity.ok(ApiResponse.success(analysis)); } - + /** - * 취향 프로필 업데이트 API + * 취향 프로필 업데이트 */ @PostMapping("/update") @Operation(summary = "취향 프로필 업데이트", description = "사용자의 리뷰 데이터를 기반으로 취향 프로필을 업데이트합니다.") - public ResponseEntity updateTasteProfile(Authentication authentication) { + public ResponseEntity> updateTasteProfile( + Authentication authentication, + @Valid @RequestBody TasteUpdateRequest request) { + Long memberId = Long.valueOf(authentication.getName()); - tasteAnalysisUseCase.updateTasteProfile(memberId); - return ResponseEntity.ok(SuccessResponse.of("취향 프로필이 업데이트되었습니다")); + tasteAnalysisUseCase.updateTasteProfile(memberId, request); + + return ResponseEntity.ok(ApiResponse.success("취향 프로필이 업데이트되었습니다")); } -} + + /** + * 가용한 취향 태그 조회 + */ + @GetMapping("/tags") + @Operation(summary = "취향 태그 목록 조회", description = "선택 가능한 취향 태그 목록을 조회합니다.") + public ResponseEntity>> getAvailablePreferenceTags() { + + List tags = tasteAnalysisUseCase.getAvailablePreferenceTags(); + + return ResponseEntity.ok(ApiResponse.success(tags)); + } +} \ No newline at end of file diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/request/RecommendStoreRequest.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/request/RecommendStoreRequest.java index 281e88c..ea12069 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/request/RecommendStoreRequest.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/request/RecommendStoreRequest.java @@ -1,10 +1,10 @@ package com.ktds.hi.recommend.infra.dto.request; import io.swagger.v3.oas.annotations.media.Schema; -import jakarta.validation.constraints.NotNull; -import lombok.AllArgsConstructor; +import jakarta.validation.constraints.*; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import java.util.List; @@ -12,46 +12,36 @@ import java.util.List; * 매장 추천 요청 DTO */ @Getter +@Setter @NoArgsConstructor -@AllArgsConstructor @Schema(description = "매장 추천 요청") public class RecommendStoreRequest { - + @NotNull(message = "위도는 필수입니다") + @DecimalMin(value = "-90.0", message = "위도는 -90도 이상이어야 합니다") + @DecimalMax(value = "90.0", message = "위도는 90도 이하여야 합니다") @Schema(description = "위도", example = "37.5665") private Double latitude; - + @NotNull(message = "경도는 필수입니다") + @DecimalMin(value = "-180.0", message = "경도는 -180도 이상이어야 합니다") + @DecimalMax(value = "180.0", message = "경도는 180도 이하여야 합니다") @Schema(description = "경도", example = "126.9780") private Double longitude; - - @Schema(description = "검색 반경(미터)", example = "5000", defaultValue = "5000") + + @Min(value = 100, message = "반경은 최소 100m 이상이어야 합니다") + @Max(value = 50000, message = "반경은 최대 50km 이하여야 합니다") + @Schema(description = "검색 반경 (미터)", example = "5000") private Integer radius = 5000; - - @Schema(description = "선호 카테고리", example = "[\"한식\", \"일식\"]") - private List preferredCategories; - - @Schema(description = "가격 범위", example = "MEDIUM") - private String priceRange; - - @Schema(description = "추천 개수", example = "10", defaultValue = "10") + + @Schema(description = "카테고리 필터", example = "한식") + private String category; + + @Schema(description = "취향 태그 목록", example = "[\"매운맛\", \"혼밥\"]") + private List tags; + + @Min(value = 1, message = "최소 1개 이상의 결과가 필요합니다") + @Max(value = 50, message = "최대 50개까지 조회 가능합니다") + @Schema(description = "결과 개수", example = "10") private Integer limit = 10; - - /** - * 유효성 검증 - */ - public void validate() { - if (latitude == null || longitude == null) { - throw new IllegalArgumentException("위도와 경도는 필수입니다"); - } - if (latitude < -90 || latitude > 90) { - throw new IllegalArgumentException("위도는 -90과 90 사이여야 합니다"); - } - if (longitude < -180 || longitude > 180) { - throw new IllegalArgumentException("경도는 -180과 180 사이여야 합니다"); - } - if (radius != null && radius <= 0) { - throw new IllegalArgumentException("검색 반경은 0보다 커야 합니다"); - } - } -} +} \ No newline at end of file diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/request/TasteUpdateRequest.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/request/TasteUpdateRequest.java new file mode 100644 index 0000000..428a96a --- /dev/null +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/request/TasteUpdateRequest.java @@ -0,0 +1,26 @@ +package com.ktds.hi.recommend.infra.dto.request; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotEmpty; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; + +/** + * 취향 업데이트 요청 DTO + */ +@Getter +@Setter +@NoArgsConstructor +@Schema(description = "취향 프로필 업데이트 요청") +public class TasteUpdateRequest { + + @NotEmpty(message = "선호 카테고리는 최소 1개 이상 선택해야 합니다") + @Schema(description = "선호 카테고리 목록", example = "[\"한식\", \"일식\"]") + private List preferredCategories; + + @Schema(description = "선호 태그 목록", example = "[\"매운맛\", \"혼밥\", \"가성비\"]") + private List preferredTags; +} \ No newline at end of file diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/PreferenceTagResponse.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/PreferenceTagResponse.java new file mode 100644 index 0000000..b12b5e0 --- /dev/null +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/PreferenceTagResponse.java @@ -0,0 +1,34 @@ +package com.ktds.hi.recommend.infra.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +/** + * 취향 태그 응답 DTO + */ +@Getter +@Builder +@Schema(description = "취향 태그 응답") +public class PreferenceTagResponse { + + @Schema(description = "태그명", example = "매운맛") + private String tagName; + + @Schema(description = "태그 아이콘", example = "🌶️") + private String icon; + + @Schema(description = "태그 설명", example = "매운 음식을 선호") + private String description; + + /** + * 정적 생성 메서드 + */ + public static PreferenceTagResponse of(String tagName, String icon, String description) { + return PreferenceTagResponse.builder() + .tagName(tagName) + .icon(icon) + .description(description) + .build(); + } +} \ No newline at end of file diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/RecommendStoreResponse.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/RecommendStoreResponse.java index bbd78b5..fae0e29 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/RecommendStoreResponse.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/RecommendStoreResponse.java @@ -1,50 +1,55 @@ package com.ktds.hi.recommend.infra.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import java.util.List; /** - * 매장 추천 응답 DTO + * 추천 매장 응답 DTO */ @Getter @Builder -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "매장 추천 응답") +@Schema(description = "추천 매장 응답") public class RecommendStoreResponse { - - @Schema(description = "매장 ID") + + @Schema(description = "매장 ID", example = "1") private Long storeId; - - @Schema(description = "매장명") + + @Schema(description = "매장명", example = "맛있는 김치찌개") private String storeName; - - @Schema(description = "주소") - private String address; - - @Schema(description = "카테고리") + + @Schema(description = "카테고리", example = "한식") private String category; - - @Schema(description = "태그 목록") - private List tags; - - @Schema(description = "평점") - private Double rating; - - @Schema(description = "리뷰 수") + + @Schema(description = "주소", example = "서울시 강남구 테헤란로 123") + private String address; + + @Schema(description = "위도", example = "37.5665") + private Double latitude; + + @Schema(description = "경도", example = "126.9780") + private Double longitude; + + @Schema(description = "평균 평점", example = "4.5") + private Double averageRating; + + @Schema(description = "리뷰 수", example = "127") private Integer reviewCount; - - @Schema(description = "거리(미터)") - private Double distance; - - @Schema(description = "추천 점수") + + @Schema(description = "추천 점수", example = "0.85") private Double recommendScore; - - @Schema(description = "추천 이유") + + @Schema(description = "추천 이유", example = "좋아하는 '매운맛' 태그에 맞는 매장입니다") private String recommendReason; + + @Schema(description = "거리 (미터)", example = "1200") + private Double distance; + + @Schema(description = "매장 이미지 URL 목록") + private List imageUrls; + + @Schema(description = "태그 목록", example = "[\"매운맛\", \"혼밥\"]") + private List tags; } diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/StoreDetailResponse.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/StoreDetailResponse.java new file mode 100644 index 0000000..b0c6c63 --- /dev/null +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/StoreDetailResponse.java @@ -0,0 +1,58 @@ +package com.ktds.hi.recommend.infra.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Builder; +import lombok.Getter; + +import java.util.List; + +/** + * 매장 상세 정보 응답 DTO + */ +@Getter +@Builder +@Schema(description = "매장 상세 정보 응답") +public class StoreDetailResponse { + + @Schema(description = "매장 ID", example = "1") + private Long storeId; + + @Schema(description = "매장명", example = "맛있는 김치찌개") + private String storeName; + + @Schema(description = "카테고리", example = "한식") + private String category; + + @Schema(description = "주소", example = "서울시 강남구 테헤란로 123") + private String address; + + @Schema(description = "전화번호", example = "02-1234-5678") + private String phoneNumber; + + @Schema(description = "위도", example = "37.5665") + private Double latitude; + + @Schema(description = "경도", example = "126.9780") + private Double longitude; + + @Schema(description = "평균 평점", example = "4.5") + private Double averageRating; + + @Schema(description = "리뷰 수", example = "127") + private Integer reviewCount; + + @Schema(description = "매장 이미지 URL 목록") + private List imageUrls; + + @Schema(description = "운영시간", example = "10:00-22:00") + private String operatingHours; + + @Schema(description = "AI 요약", example = "친절한 서비스와 맛있는 김치찌개로 유명한 곳입니다") + private String aiSummary; + + @Schema(description = "긍정 키워드", example = "[\"맛있다\", \"친절하다\", \"깔끔하다\"]") + private List topPositiveKeywords; + + @Schema(description = "부정 키워드", example = "[\"시끄럽다\", \"대기시간\"]") + private List topNegativeKeywords; +} \ No newline at end of file diff --git a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/TasteAnalysisResponse.java b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/TasteAnalysisResponse.java index 5c57c57..f20798a 100644 --- a/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/TasteAnalysisResponse.java +++ b/recommend/src/main/java/com/ktds/hi/recommend/infra/dto/response/TasteAnalysisResponse.java @@ -1,46 +1,42 @@ package com.ktds.hi.recommend.infra.dto.response; import io.swagger.v3.oas.annotations.media.Schema; -import lombok.AllArgsConstructor; import lombok.Builder; import lombok.Getter; -import lombok.NoArgsConstructor; import java.time.LocalDateTime; import java.util.List; import java.util.Map; /** - * 취향 분석 응답 DTO + * 취향 분석 결과 응답 DTO */ @Getter @Builder -@NoArgsConstructor -@AllArgsConstructor -@Schema(description = "취향 분석 응답") +@Schema(description = "취향 분석 결과 응답") public class TasteAnalysisResponse { - - @Schema(description = "회원 ID") + + @Schema(description = "회원 ID", example = "1") private Long memberId; - - @Schema(description = "선호 카테고리") + + @Schema(description = "선호 카테고리 목록", example = "[\"한식\", \"일식\"]") private List preferredCategories; - - @Schema(description = "최고 선호 카테고리") + + @Schema(description = "최고 선호 카테고리", example = "한식") private String topCategory; - + @Schema(description = "카테고리별 점수") private Map categoryScores; - - @Schema(description = "선호 태그") + + @Schema(description = "선호 태그 목록", example = "[\"매운맛\", \"혼밥\"]") private List preferredTags; - - @Schema(description = "가격 선호도") - private Double pricePreference; - - @Schema(description = "거리 선호도") - private Double distancePreference; - + @Schema(description = "분석 일시") private LocalDateTime analysisDate; + + @Schema(description = "신뢰도", example = "0.75") + private Double confidence; + + @Schema(description = "추천사항", example = "[\"한식 카테고리의 새로운 매장을 시도해보세요\"]") + private List recommendations; } diff --git a/recommend/src/main/resources/application.yml b/recommend/src/main/resources/application.yml index edb7451..7a06131 100644 --- a/recommend/src/main/resources/application.yml +++ b/recommend/src/main/resources/application.yml @@ -1,3 +1,4 @@ +# recommend/src/main/resources/application.yml server: port: ${RECOMMEND_SERVICE_PORT:8085} @@ -5,12 +6,24 @@ spring: application: name: recommend-service + # 프로필 설정 + profiles: + active: ${SPRING_PROFILES_ACTIVE:local} + + # 데이터베이스 설정 datasource: url: ${RECOMMEND_DB_URL:jdbc:postgresql://20.249.162.245:5432/hiorder_recommend} username: ${RECOMMEND_DB_USERNAME:hiorder_user} password: ${RECOMMEND_DB_PASSWORD:hiorder_pass} driver-class-name: org.postgresql.Driver + hikari: + connection-timeout: 20000 + maximum-pool-size: 10 + minimum-idle: 5 + idle-timeout: 300000 + pool-name: RecommendHikariCP + # JPA 설정 jpa: hibernate: ddl-auto: ${JPA_DDL_AUTO:validate} @@ -19,27 +32,206 @@ spring: hibernate: format_sql: true dialect: org.hibernate.dialect.PostgreSQLDialect - + jdbc: + batch_size: 20 + order_inserts: true + order_updates: true + open-in-view: false + + # Redis 설정 (올바른 구조) data: redis: host: ${REDIS_HOST:localhost} port: ${REDIS_PORT:6379} password: ${REDIS_PASSWORD:} + timeout: 2000ms + database: ${REDIS_DATABASE:0} + lettuce: + pool: + max-active: 8 + max-idle: 8 + min-idle: 2 + max-wait: -1ms + shutdown-timeout: 100ms -recommendation: - cache-ttl: 3600 # 1시간 - max-recommendations: 20 - default-radius: 5000 # 5km +# 외부 서비스 URL 설정 +services: + store: + url: ${STORE_SERVICE_URL:http://store-service:8082} + review: + url: ${REVIEW_SERVICE_URL:http://review-service:8083} + member: + url: ${MEMBER_SERVICE_URL:http://member-service:8081} -location: - google-maps-api-key: ${GOOGLE_MAPS_API_KEY:} - -hiorder-api: - base-url: ${HIORDER_API_BASE_URL:https://api.hiorder.com} - api-key: ${HIORDER_API_KEY:} +# Feign 설정 +feign: + client: + config: + default: + connectTimeout: 5000 + readTimeout: 10000 + loggerLevel: basic + store-service: + connectTimeout: 3000 + readTimeout: 8000 + review-service: + connectTimeout: 3000 + readTimeout: 8000 + circuitbreaker: + enabled: true + compression: + request: + enabled: true + response: + enabled: true +# Circuit Breaker 설정 +resilience4j: + circuitbreaker: + instances: + store-service: + failure-rate-threshold: 50 + wait-duration-in-open-state: 30000 + sliding-window-size: 10 + minimum-number-of-calls: 5 + review-service: + failure-rate-threshold: 50 + wait-duration-in-open-state: 30000 + sliding-window-size: 10 + minimum-number-of-calls: 5 + retry: + instances: + store-service: + max-attempts: 3 + wait-duration: 1000 + review-service: + max-attempts: 3 + wait-duration: 1000 + +# 추천 알고리즘 설정 +recommend: + algorithm: + distance-weight: 0.3 + rating-weight: 0.3 + taste-weight: 0.4 + max-distance: 50000 # 50km + default-radius: 5000 # 5km + cache: + ttl: + recommendation: 30m + store-detail: 1h + taste-analysis: 6h + popular-stores: 2h + batch: + size: 100 + max-concurrent: 5 + +# Actuator 설정 +management: + endpoints: + web: + exposure: + include: health,info,metrics,prometheus,configprops + base-path: /actuator + endpoint: + health: + show-details: when-authorized + show-components: always + metrics: + enabled: true + metrics: + export: + prometheus: + enabled: true + tags: + application: ${spring.application.name} + +# Swagger/OpenAPI 설정 springdoc: api-docs: path: /api-docs + enabled: true swagger-ui: path: /swagger-ui.html + tags-sorter: alpha + operations-sorter: alpha + display-request-duration: true + display-operation-id: true + show-actuator: false + +# 로깅 설정 +logging: + level: + root: ${LOG_LEVEL_ROOT:INFO} + com.ktds.hi.recommend: ${LOG_LEVEL:INFO} + org.springframework.cloud.openfeign: ${LOG_LEVEL_FEIGN:DEBUG} + org.springframework.web: ${LOG_LEVEL_WEB:INFO} + org.springframework.data.redis: ${LOG_LEVEL_REDIS:INFO} + org.hibernate.SQL: ${LOG_LEVEL_SQL:INFO} + org.hibernate.type.descriptor.sql.BasicBinder: ${LOG_LEVEL_SQL_PARAM:INFO} + pattern: + console: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId},%X{spanId}] %logger{36} - %msg%n" + file: "%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level [%X{traceId},%X{spanId}] %logger{36} - %msg%n" + file: + name: ${LOG_FILE_PATH:./logs/recommend-service.log} + max-size: 100MB + max-history: 30 + +# Security 설정 +security: + jwt: + secret: ${JWT_SECRET:hiorder-recommend-secret-key-2024} + expiration: ${JWT_EXPIRATION:86400000} # 24시간 + cors: + allowed-origins: ${CORS_ALLOWED_ORIGINS:http://localhost:3000,http://localhost:8080} + allowed-methods: GET,POST,PUT,DELETE,OPTIONS + allowed-headers: "*" + allow-credentials: true + +--- +# Local 환경 설정 +spring: + config: + activate: + on-profile: local + jpa: + show-sql: true + hibernate: + ddl-auto: create-drop + +logging: + level: + com.ktds.hi.recommend: DEBUG + org.springframework.web: DEBUG + +--- +# Development 환경 설정 +spring: + config: + activate: + on-profile: dev + jpa: + show-sql: true + hibernate: + ddl-auto: update + +logging: + level: + com.ktds.hi.recommend: DEBUG + +--- +# Production 환경 설정 +spring: + config: + activate: + on-profile: prod + jpa: + show-sql: false + hibernate: + ddl-auto: validate + +logging: + level: + root: WARN + com.ktds.hi.recommend: INFO + org.springframework.cloud.openfeign: INFO \ No newline at end of file