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
|
// AI and Location Services
|
||||||
implementation 'org.springframework.boot:spring-boot-starter-webflux'
|
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,20 +2,20 @@ package com.ktds.hi.recommend;
|
|||||||
|
|
||||||
import org.springframework.boot.SpringApplication;
|
import org.springframework.boot.SpringApplication;
|
||||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||||
|
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
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
|
@EnableJpaAuditing
|
||||||
public class RecommendServiceApplication {
|
public class RecommendServiceApplication {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(RecommendServiceApplication.class, 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;
|
package com.ktds.hi.recommend.infra.controller;
|
||||||
|
|
||||||
import com.ktds.hi.recommend.biz.usecase.in.StoreRecommendUseCase;
|
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.request.RecommendStoreRequest;
|
||||||
import com.ktds.hi.recommend.infra.dto.response.RecommendStoreResponse;
|
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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
import jakarta.validation.Valid;
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.web.PageableDefault;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
@ -14,55 +20,93 @@ import org.springframework.web.bind.annotation.*;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 추천 컨트롤러 클래스
|
* 매장 추천 컨트롤러
|
||||||
* 매장 추천 관련 API를 제공
|
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/recommend")
|
@RequestMapping("/api/recommend/stores")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "매장 추천 API", description = "사용자 취향 기반 매장 추천 관련 API")
|
@Tag(name = "매장 추천 API", description = "사용자 취향 기반 매장 추천 관련 API")
|
||||||
public class StoreRecommendController {
|
public class StoreRecommendController {
|
||||||
|
|
||||||
private final StoreRecommendUseCase storeRecommendUseCase;
|
private final StoreRecommendUseCase storeRecommendUseCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 취향 기반 매장 추천 API
|
* 개인화 매장 추천
|
||||||
*/
|
*/
|
||||||
@PostMapping("/stores")
|
@PostMapping
|
||||||
@Operation(summary = "매장 추천", description = "사용자 취향과 위치를 기반으로 매장을 추천합니다.")
|
@Operation(summary = "개인화 매장 추천", description = "사용자 취향과 위치를 기반으로 매장을 추천합니다.")
|
||||||
public ResponseEntity<List<RecommendStoreResponse>> recommendStores(Authentication authentication,
|
public ResponseEntity<ApiResponse<List<RecommendStoreResponse>>> recommendStores(
|
||||||
@Valid @RequestBody RecommendStoreRequest request) {
|
Authentication authentication,
|
||||||
|
@Valid @RequestBody RecommendStoreRequest request) {
|
||||||
|
|
||||||
Long memberId = Long.valueOf(authentication.getName());
|
Long memberId = Long.valueOf(authentication.getName());
|
||||||
List<RecommendStoreResponse> recommendations = storeRecommendUseCase.recommendStores(memberId, request);
|
List<RecommendStoreResponse> recommendations =
|
||||||
return ResponseEntity.ok(recommendations);
|
storeRecommendUseCase.recommendPersonalizedStores(memberId, request);
|
||||||
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(recommendations));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 위치 기반 매장 추천 API
|
* 위치 기반 매장 추천
|
||||||
*/
|
*/
|
||||||
@GetMapping("/stores/nearby")
|
@GetMapping("/nearby")
|
||||||
@Operation(summary = "주변 매장 추천", description = "현재 위치 기반으로 주변 매장을 추천합니다.")
|
@Operation(summary = "주변 매장 추천", description = "현재 위치 기반으로 주변 매장을 추천합니다.")
|
||||||
public ResponseEntity<List<RecommendStoreResponse>> recommendNearbyStores(
|
public ResponseEntity<ApiResponse<PageResponse<RecommendStoreResponse>>> recommendNearbyStores(
|
||||||
@RequestParam Double latitude,
|
@RequestParam Double latitude,
|
||||||
@RequestParam Double longitude,
|
@RequestParam Double longitude,
|
||||||
@RequestParam(defaultValue = "5000") Integer radius) {
|
@RequestParam(defaultValue = "5000") Integer radius,
|
||||||
|
@RequestParam(required = false) String category,
|
||||||
List<RecommendStoreResponse> recommendations = storeRecommendUseCase
|
@PageableDefault(size = 20) Pageable pageable) {
|
||||||
.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 = "카테고리별 인기 매장을 추천합니다.")
|
@Operation(summary = "인기 매장 추천", description = "카테고리별 인기 매장을 추천합니다.")
|
||||||
public ResponseEntity<List<RecommendStoreResponse>> recommendPopularStores(
|
public ResponseEntity<ApiResponse<List<RecommendStoreResponse>>> recommendPopularStores(
|
||||||
@RequestParam(required = false) String category,
|
@RequestParam(required = false) String category,
|
||||||
@RequestParam(defaultValue = "10") Integer limit) {
|
@RequestParam(defaultValue = "10") Integer limit) {
|
||||||
|
|
||||||
List<RecommendStoreResponse> recommendations = storeRecommendUseCase
|
List<RecommendStoreResponse> recommendations =
|
||||||
.recommendPopularStores(category, limit);
|
storeRecommendUseCase.recommendPopularStores(category, limit);
|
||||||
return ResponseEntity.ok(recommendations);
|
|
||||||
|
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,46 +1,69 @@
|
|||||||
package com.ktds.hi.recommend.infra.controller;
|
package com.ktds.hi.recommend.infra.controller;
|
||||||
|
|
||||||
import com.ktds.hi.recommend.biz.usecase.in.TasteAnalysisUseCase;
|
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.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.Operation;
|
||||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||||
|
import jakarta.validation.Valid;
|
||||||
import lombok.RequiredArgsConstructor;
|
import lombok.RequiredArgsConstructor;
|
||||||
import org.springframework.http.ResponseEntity;
|
import org.springframework.http.ResponseEntity;
|
||||||
import org.springframework.security.core.Authentication;
|
import org.springframework.security.core.Authentication;
|
||||||
import org.springframework.web.bind.annotation.*;
|
import org.springframework.web.bind.annotation.*;
|
||||||
|
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 취향 분석 컨트롤러 클래스
|
* 취향 분석 컨트롤러
|
||||||
* 사용자 취향 분석 관련 API를 제공
|
|
||||||
*/
|
*/
|
||||||
@RestController
|
@RestController
|
||||||
@RequestMapping("/api/recommend/taste")
|
@RequestMapping("/api/recommend/taste")
|
||||||
@RequiredArgsConstructor
|
@RequiredArgsConstructor
|
||||||
@Tag(name = "취향 분석 API", description = "사용자 취향 분석 관련 API")
|
@Tag(name = "취향 분석 API", description = "사용자 취향 분석 관련 API")
|
||||||
public class TasteAnalysisController {
|
public class TasteAnalysisController {
|
||||||
|
|
||||||
private final TasteAnalysisUseCase tasteAnalysisUseCase;
|
private final TasteAnalysisUseCase tasteAnalysisUseCase;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 사용자 취향 분석 조회 API
|
* 사용자 취향 분석 조회
|
||||||
*/
|
*/
|
||||||
@GetMapping("/analysis")
|
@GetMapping("/analysis")
|
||||||
@Operation(summary = "취향 분석 조회", description = "현재 로그인한 사용자의 취향 분석 결과를 조회합니다.")
|
@Operation(summary = "취향 분석 조회", description = "현재 로그인한 사용자의 취향 분석 결과를 조회합니다.")
|
||||||
public ResponseEntity<TasteAnalysisResponse> getMemberTasteAnalysis(Authentication authentication) {
|
public ResponseEntity<ApiResponse<TasteAnalysisResponse>> getMemberTasteAnalysis(
|
||||||
|
Authentication authentication) {
|
||||||
|
|
||||||
Long memberId = Long.valueOf(authentication.getName());
|
Long memberId = Long.valueOf(authentication.getName());
|
||||||
TasteAnalysisResponse analysis = tasteAnalysisUseCase.analyzeMemberTaste(memberId);
|
TasteAnalysisResponse analysis = tasteAnalysisUseCase.analyzeMemberTaste(memberId);
|
||||||
return ResponseEntity.ok(analysis);
|
|
||||||
|
return ResponseEntity.ok(ApiResponse.success(analysis));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 취향 프로필 업데이트 API
|
* 취향 프로필 업데이트
|
||||||
*/
|
*/
|
||||||
@PostMapping("/update")
|
@PostMapping("/update")
|
||||||
@Operation(summary = "취향 프로필 업데이트", description = "사용자의 리뷰 데이터를 기반으로 취향 프로필을 업데이트합니다.")
|
@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());
|
Long memberId = Long.valueOf(authentication.getName());
|
||||||
tasteAnalysisUseCase.updateTasteProfile(memberId);
|
tasteAnalysisUseCase.updateTasteProfile(memberId, request);
|
||||||
return ResponseEntity.ok(SuccessResponse.of("취향 프로필이 업데이트되었습니다"));
|
|
||||||
|
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;
|
package com.ktds.hi.recommend.infra.dto.request;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import jakarta.validation.constraints.NotNull;
|
import jakarta.validation.constraints.*;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
import lombok.NoArgsConstructor;
|
||||||
|
import lombok.Setter;
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
@ -12,46 +12,36 @@ import java.util.List;
|
|||||||
* 매장 추천 요청 DTO
|
* 매장 추천 요청 DTO
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
|
@Setter
|
||||||
@NoArgsConstructor
|
@NoArgsConstructor
|
||||||
@AllArgsConstructor
|
|
||||||
@Schema(description = "매장 추천 요청")
|
@Schema(description = "매장 추천 요청")
|
||||||
public class RecommendStoreRequest {
|
public class RecommendStoreRequest {
|
||||||
|
|
||||||
@NotNull(message = "위도는 필수입니다")
|
@NotNull(message = "위도는 필수입니다")
|
||||||
|
@DecimalMin(value = "-90.0", message = "위도는 -90도 이상이어야 합니다")
|
||||||
|
@DecimalMax(value = "90.0", message = "위도는 90도 이하여야 합니다")
|
||||||
@Schema(description = "위도", example = "37.5665")
|
@Schema(description = "위도", example = "37.5665")
|
||||||
private Double latitude;
|
private Double latitude;
|
||||||
|
|
||||||
@NotNull(message = "경도는 필수입니다")
|
@NotNull(message = "경도는 필수입니다")
|
||||||
|
@DecimalMin(value = "-180.0", message = "경도는 -180도 이상이어야 합니다")
|
||||||
|
@DecimalMax(value = "180.0", message = "경도는 180도 이하여야 합니다")
|
||||||
@Schema(description = "경도", example = "126.9780")
|
@Schema(description = "경도", example = "126.9780")
|
||||||
private Double longitude;
|
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;
|
private Integer radius = 5000;
|
||||||
|
|
||||||
@Schema(description = "선호 카테고리", example = "[\"한식\", \"일식\"]")
|
@Schema(description = "카테고리 필터", example = "한식")
|
||||||
private List<String> preferredCategories;
|
private String category;
|
||||||
|
|
||||||
@Schema(description = "가격 범위", example = "MEDIUM")
|
@Schema(description = "취향 태그 목록", example = "[\"매운맛\", \"혼밥\"]")
|
||||||
private String priceRange;
|
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;
|
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;
|
package com.ktds.hi.recommend.infra.dto.response;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 매장 추천 응답 DTO
|
* 추천 매장 응답 DTO
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@Schema(description = "추천 매장 응답")
|
||||||
@AllArgsConstructor
|
|
||||||
@Schema(description = "매장 추천 응답")
|
|
||||||
public class RecommendStoreResponse {
|
public class RecommendStoreResponse {
|
||||||
|
|
||||||
@Schema(description = "매장 ID")
|
@Schema(description = "매장 ID", example = "1")
|
||||||
private Long storeId;
|
private Long storeId;
|
||||||
|
|
||||||
@Schema(description = "매장명")
|
@Schema(description = "매장명", example = "맛있는 김치찌개")
|
||||||
private String storeName;
|
private String storeName;
|
||||||
|
|
||||||
@Schema(description = "주소")
|
@Schema(description = "카테고리", example = "한식")
|
||||||
private String address;
|
|
||||||
|
|
||||||
@Schema(description = "카테고리")
|
|
||||||
private String category;
|
private String category;
|
||||||
|
|
||||||
@Schema(description = "태그 목록")
|
@Schema(description = "주소", example = "서울시 강남구 테헤란로 123")
|
||||||
private List<String> tags;
|
private String address;
|
||||||
|
|
||||||
@Schema(description = "평점")
|
@Schema(description = "위도", example = "37.5665")
|
||||||
private Double rating;
|
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;
|
private Integer reviewCount;
|
||||||
|
|
||||||
@Schema(description = "거리(미터)")
|
@Schema(description = "추천 점수", example = "0.85")
|
||||||
private Double distance;
|
|
||||||
|
|
||||||
@Schema(description = "추천 점수")
|
|
||||||
private Double recommendScore;
|
private Double recommendScore;
|
||||||
|
|
||||||
@Schema(description = "추천 이유")
|
@Schema(description = "추천 이유", example = "좋아하는 '매운맛' 태그에 맞는 매장입니다")
|
||||||
private String recommendReason;
|
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;
|
package com.ktds.hi.recommend.infra.dto.response;
|
||||||
|
|
||||||
import io.swagger.v3.oas.annotations.media.Schema;
|
import io.swagger.v3.oas.annotations.media.Schema;
|
||||||
import lombok.AllArgsConstructor;
|
|
||||||
import lombok.Builder;
|
import lombok.Builder;
|
||||||
import lombok.Getter;
|
import lombok.Getter;
|
||||||
import lombok.NoArgsConstructor;
|
|
||||||
|
|
||||||
import java.time.LocalDateTime;
|
import java.time.LocalDateTime;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 취향 분석 응답 DTO
|
* 취향 분석 결과 응답 DTO
|
||||||
*/
|
*/
|
||||||
@Getter
|
@Getter
|
||||||
@Builder
|
@Builder
|
||||||
@NoArgsConstructor
|
@Schema(description = "취향 분석 결과 응답")
|
||||||
@AllArgsConstructor
|
|
||||||
@Schema(description = "취향 분석 응답")
|
|
||||||
public class TasteAnalysisResponse {
|
public class TasteAnalysisResponse {
|
||||||
|
|
||||||
@Schema(description = "회원 ID")
|
@Schema(description = "회원 ID", example = "1")
|
||||||
private Long memberId;
|
private Long memberId;
|
||||||
|
|
||||||
@Schema(description = "선호 카테고리")
|
@Schema(description = "선호 카테고리 목록", example = "[\"한식\", \"일식\"]")
|
||||||
private List<String> preferredCategories;
|
private List<String> preferredCategories;
|
||||||
|
|
||||||
@Schema(description = "최고 선호 카테고리")
|
@Schema(description = "최고 선호 카테고리", example = "한식")
|
||||||
private String topCategory;
|
private String topCategory;
|
||||||
|
|
||||||
@Schema(description = "카테고리별 점수")
|
@Schema(description = "카테고리별 점수")
|
||||||
private Map<String, Double> categoryScores;
|
private Map<String, Double> categoryScores;
|
||||||
|
|
||||||
@Schema(description = "선호 태그")
|
@Schema(description = "선호 태그 목록", example = "[\"매운맛\", \"혼밥\"]")
|
||||||
private List<String> preferredTags;
|
private List<String> preferredTags;
|
||||||
|
|
||||||
@Schema(description = "가격 선호도")
|
|
||||||
private Double pricePreference;
|
|
||||||
|
|
||||||
@Schema(description = "거리 선호도")
|
|
||||||
private Double distancePreference;
|
|
||||||
|
|
||||||
@Schema(description = "분석 일시")
|
@Schema(description = "분석 일시")
|
||||||
private LocalDateTime analysisDate;
|
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:
|
server:
|
||||||
port: ${RECOMMEND_SERVICE_PORT:8085}
|
port: ${RECOMMEND_SERVICE_PORT:8085}
|
||||||
|
|
||||||
@ -5,12 +6,24 @@ spring:
|
|||||||
application:
|
application:
|
||||||
name: recommend-service
|
name: recommend-service
|
||||||
|
|
||||||
|
# 프로필 설정
|
||||||
|
profiles:
|
||||||
|
active: ${SPRING_PROFILES_ACTIVE:local}
|
||||||
|
|
||||||
|
# 데이터베이스 설정
|
||||||
datasource:
|
datasource:
|
||||||
url: ${RECOMMEND_DB_URL:jdbc:postgresql://20.249.162.245:5432/hiorder_recommend}
|
url: ${RECOMMEND_DB_URL:jdbc:postgresql://20.249.162.245:5432/hiorder_recommend}
|
||||||
username: ${RECOMMEND_DB_USERNAME:hiorder_user}
|
username: ${RECOMMEND_DB_USERNAME:hiorder_user}
|
||||||
password: ${RECOMMEND_DB_PASSWORD:hiorder_pass}
|
password: ${RECOMMEND_DB_PASSWORD:hiorder_pass}
|
||||||
driver-class-name: org.postgresql.Driver
|
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:
|
jpa:
|
||||||
hibernate:
|
hibernate:
|
||||||
ddl-auto: ${JPA_DDL_AUTO:validate}
|
ddl-auto: ${JPA_DDL_AUTO:validate}
|
||||||
@ -19,27 +32,206 @@ spring:
|
|||||||
hibernate:
|
hibernate:
|
||||||
format_sql: true
|
format_sql: true
|
||||||
dialect: org.hibernate.dialect.PostgreSQLDialect
|
dialect: org.hibernate.dialect.PostgreSQLDialect
|
||||||
|
jdbc:
|
||||||
|
batch_size: 20
|
||||||
|
order_inserts: true
|
||||||
|
order_updates: true
|
||||||
|
open-in-view: false
|
||||||
|
|
||||||
|
# Redis 설정 (올바른 구조)
|
||||||
data:
|
data:
|
||||||
redis:
|
redis:
|
||||||
host: ${REDIS_HOST:localhost}
|
host: ${REDIS_HOST:localhost}
|
||||||
port: ${REDIS_PORT:6379}
|
port: ${REDIS_PORT:6379}
|
||||||
password: ${REDIS_PASSWORD:}
|
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:
|
# 외부 서비스 URL 설정
|
||||||
cache-ttl: 3600 # 1시간
|
services:
|
||||||
max-recommendations: 20
|
store:
|
||||||
default-radius: 5000 # 5km
|
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:
|
# Feign 설정
|
||||||
google-maps-api-key: ${GOOGLE_MAPS_API_KEY:}
|
feign:
|
||||||
|
client:
|
||||||
hiorder-api:
|
config:
|
||||||
base-url: ${HIORDER_API_BASE_URL:https://api.hiorder.com}
|
default:
|
||||||
api-key: ${HIORDER_API_KEY:}
|
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:
|
springdoc:
|
||||||
api-docs:
|
api-docs:
|
||||||
path: /api-docs
|
path: /api-docs
|
||||||
|
enabled: true
|
||||||
swagger-ui:
|
swagger-ui:
|
||||||
path: /swagger-ui.html
|
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