Merge branch 'main' of https://github.com/dg04-hi/hi-backend
This commit is contained in:
commit
fc43d07e47
@ -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'
|
||||
}
|
||||
|
||||
@ -2,19 +2,19 @@ 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);
|
||||
}
|
||||
|
||||
@ -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,11 +20,10 @@ 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 {
|
||||
@ -26,43 +31,82 @@ public class StoreRecommendController {
|
||||
private final StoreRecommendUseCase storeRecommendUseCase;
|
||||
|
||||
/**
|
||||
* 사용자 취향 기반 매장 추천 API
|
||||
* 개인화 매장 추천
|
||||
*/
|
||||
@PostMapping("/stores")
|
||||
@Operation(summary = "매장 추천", description = "사용자 취향과 위치를 기반으로 매장을 추천합니다.")
|
||||
public ResponseEntity<List<RecommendStoreResponse>> recommendStores(Authentication authentication,
|
||||
@Valid @RequestBody RecommendStoreRequest request) {
|
||||
@PostMapping
|
||||
@Operation(summary = "개인화 매장 추천", description = "사용자 취향과 위치를 기반으로 매장을 추천합니다.")
|
||||
public ResponseEntity<ApiResponse<List<RecommendStoreResponse>>> recommendStores(
|
||||
Authentication authentication,
|
||||
@Valid @RequestBody RecommendStoreRequest request) {
|
||||
|
||||
Long memberId = Long.valueOf(authentication.getName());
|
||||
List<RecommendStoreResponse> recommendations = storeRecommendUseCase.recommendStores(memberId, request);
|
||||
return ResponseEntity.ok(recommendations);
|
||||
List<RecommendStoreResponse> recommendations =
|
||||
storeRecommendUseCase.recommendPersonalizedStores(memberId, request);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(recommendations));
|
||||
}
|
||||
|
||||
/**
|
||||
* 위치 기반 매장 추천 API
|
||||
* 위치 기반 매장 추천
|
||||
*/
|
||||
@GetMapping("/stores/nearby")
|
||||
@GetMapping("/nearby")
|
||||
@Operation(summary = "주변 매장 추천", description = "현재 위치 기반으로 주변 매장을 추천합니다.")
|
||||
public ResponseEntity<List<RecommendStoreResponse>> recommendNearbyStores(
|
||||
public ResponseEntity<ApiResponse<PageResponse<RecommendStoreResponse>>> recommendNearbyStores(
|
||||
@RequestParam Double latitude,
|
||||
@RequestParam Double longitude,
|
||||
@RequestParam(defaultValue = "5000") Integer radius) {
|
||||
@RequestParam(defaultValue = "5000") Integer radius,
|
||||
@RequestParam(required = false) String category,
|
||||
@PageableDefault(size = 20) Pageable pageable) {
|
||||
|
||||
List<RecommendStoreResponse> recommendations = storeRecommendUseCase
|
||||
.recommendStoresByLocation(latitude, longitude, radius);
|
||||
return ResponseEntity.ok(recommendations);
|
||||
PageResponse<RecommendStoreResponse> 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<List<RecommendStoreResponse>> recommendPopularStores(
|
||||
public ResponseEntity<ApiResponse<List<RecommendStoreResponse>>> recommendPopularStores(
|
||||
@RequestParam(required = false) String category,
|
||||
@RequestParam(defaultValue = "10") Integer limit) {
|
||||
|
||||
List<RecommendStoreResponse> recommendations = storeRecommendUseCase
|
||||
.recommendPopularStores(category, limit);
|
||||
return ResponseEntity.ok(recommendations);
|
||||
List<RecommendStoreResponse> recommendations =
|
||||
storeRecommendUseCase.recommendPopularStores(category, limit);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(recommendations));
|
||||
}
|
||||
|
||||
/**
|
||||
* 추천 매장 상세 조회
|
||||
*/
|
||||
@GetMapping("/{storeId}")
|
||||
@Operation(summary = "추천 매장 상세 조회", description = "추천된 매장의 상세 정보를 조회합니다.")
|
||||
public ResponseEntity<ApiResponse<StoreDetailResponse>> 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<ApiResponse<Void>> logRecommendClick(
|
||||
@PathVariable Long storeId,
|
||||
Authentication authentication) {
|
||||
|
||||
Long memberId = Long.valueOf(authentication.getName());
|
||||
storeRecommendUseCase.logRecommendClick(memberId, storeId);
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success());
|
||||
}
|
||||
}
|
||||
@ -1,18 +1,22 @@
|
||||
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")
|
||||
@ -23,24 +27,43 @@ public class TasteAnalysisController {
|
||||
private final TasteAnalysisUseCase tasteAnalysisUseCase;
|
||||
|
||||
/**
|
||||
* 사용자 취향 분석 조회 API
|
||||
* 사용자 취향 분석 조회
|
||||
*/
|
||||
@GetMapping("/analysis")
|
||||
@Operation(summary = "취향 분석 조회", description = "현재 로그인한 사용자의 취향 분석 결과를 조회합니다.")
|
||||
public ResponseEntity<TasteAnalysisResponse> getMemberTasteAnalysis(Authentication authentication) {
|
||||
public ResponseEntity<ApiResponse<TasteAnalysisResponse>> 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<SuccessResponse> updateTasteProfile(Authentication authentication) {
|
||||
public ResponseEntity<ApiResponse<Void>> 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<ApiResponse<List<PreferenceTagResponse>>> getAvailablePreferenceTags() {
|
||||
|
||||
List<PreferenceTagResponse> tags = tasteAnalysisUseCase.getAvailablePreferenceTags();
|
||||
|
||||
return ResponseEntity.ok(ApiResponse.success(tags));
|
||||
}
|
||||
}
|
||||
@ -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<String> preferredCategories;
|
||||
@Schema(description = "카테고리 필터", example = "한식")
|
||||
private String category;
|
||||
|
||||
@Schema(description = "가격 범위", example = "MEDIUM")
|
||||
private String priceRange;
|
||||
@Schema(description = "취향 태그 목록", example = "[\"매운맛\", \"혼밥\"]")
|
||||
private List<String> tags;
|
||||
|
||||
@Schema(description = "추천 개수", example = "10", defaultValue = "10")
|
||||
@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보다 커야 합니다");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -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<String> preferredCategories;
|
||||
|
||||
@Schema(description = "선호 태그 목록", example = "[\"매운맛\", \"혼밥\", \"가성비\"]")
|
||||
private List<String> preferredTags;
|
||||
}
|
||||
@ -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();
|
||||
}
|
||||
}
|
||||
@ -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<String> tags;
|
||||
@Schema(description = "주소", example = "서울시 강남구 테헤란로 123")
|
||||
private String address;
|
||||
|
||||
@Schema(description = "평점")
|
||||
private Double rating;
|
||||
@Schema(description = "위도", example = "37.5665")
|
||||
private Double latitude;
|
||||
|
||||
@Schema(description = "리뷰 수")
|
||||
@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<String> imageUrls;
|
||||
|
||||
@Schema(description = "태그 목록", example = "[\"매운맛\", \"혼밥\"]")
|
||||
private List<String> tags;
|
||||
}
|
||||
|
||||
@ -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<String> imageUrls;
|
||||
|
||||
@Schema(description = "운영시간", example = "10:00-22:00")
|
||||
private String operatingHours;
|
||||
|
||||
@Schema(description = "AI 요약", example = "친절한 서비스와 맛있는 김치찌개로 유명한 곳입니다")
|
||||
private String aiSummary;
|
||||
|
||||
@Schema(description = "긍정 키워드", example = "[\"맛있다\", \"친절하다\", \"깔끔하다\"]")
|
||||
private List<String> topPositiveKeywords;
|
||||
|
||||
@Schema(description = "부정 키워드", example = "[\"시끄럽다\", \"대기시간\"]")
|
||||
private List<String> topNegativeKeywords;
|
||||
}
|
||||
@ -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<String> preferredCategories;
|
||||
|
||||
@Schema(description = "최고 선호 카테고리")
|
||||
@Schema(description = "최고 선호 카테고리", example = "한식")
|
||||
private String topCategory;
|
||||
|
||||
@Schema(description = "카테고리별 점수")
|
||||
private Map<String, Double> categoryScores;
|
||||
|
||||
@Schema(description = "선호 태그")
|
||||
@Schema(description = "선호 태그 목록", example = "[\"매운맛\", \"혼밥\"]")
|
||||
private List<String> 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<String> recommendations;
|
||||
}
|
||||
|
||||
@ -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:}
|
||||
# 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
|
||||
|
||||
hiorder-api:
|
||||
base-url: ${HIORDER_API_BASE_URL:https://api.hiorder.com}
|
||||
api-key: ${HIORDER_API_KEY:}
|
||||
# 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
|
||||
Loading…
x
Reference in New Issue
Block a user