diff --git a/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheService.java b/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheService.java new file mode 100644 index 0000000..897ac9a --- /dev/null +++ b/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheService.java @@ -0,0 +1,14 @@ +package DiffLens.back_end.domain.panel.service; + +import DiffLens.back_end.domain.panel.entity.Panel; +import DiffLens.back_end.global.redis.CacheService; + +/** + * + * 패널 정보에 대한 캐시 정보를 다루는 service interface + * + * @param 캐시로 다룰 데이터 + */ +public interface PanelInfoCacheService extends CacheService { + +} diff --git a/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceImpl.java b/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceImpl.java new file mode 100644 index 0000000..4e0e333 --- /dev/null +++ b/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceImpl.java @@ -0,0 +1,38 @@ +package DiffLens.back_end.domain.panel.service; + +import DiffLens.back_end.domain.panel.entity.Panel; +import DiffLens.back_end.domain.search.repository.cache.RedisRecommendationCacheRepository; +import lombok.RequiredArgsConstructor; + +/** + * 패널 정보 캐싱 전략 + * + * - 패널 정보는 유저 당 캐싱하지 않고, 서비스 전반적으로 저장합니다. + * + */ +@RequiredArgsConstructor +public class PanelInfoCacheServiceImpl implements PanelInfoCacheService{ + + private static final Integer TTL = 10; + + // 캐시 정보를 관리하는 repository + private final RedisRecommendationCacheRepository cacheRepository; + + // 캐시 Key 접두사 + private static final String CACHE_KEY_PREFIX = "search:recommend:"; // search:recommend:{memberId} 형식으로 key 지정할 예정 + + @Override + public Panel getCacheInfo(Panel key) { + + return null; + + } + + @Override + public void saveCacheInfo(Panel data, Panel cacheInfo) { + + } + + + +} diff --git a/src/main/java/DiffLens/back_end/domain/search/controller/SearchController.java b/src/main/java/DiffLens/back_end/domain/search/controller/SearchController.java index 4bcef56..e01140b 100644 --- a/src/main/java/DiffLens/back_end/domain/search/controller/SearchController.java +++ b/src/main/java/DiffLens/back_end/domain/search/controller/SearchController.java @@ -1,6 +1,5 @@ package DiffLens.back_end.domain.search.controller; -import DiffLens.back_end.domain.search.dto.ChartDTO; import DiffLens.back_end.domain.search.dto.SearchRequestDTO; import DiffLens.back_end.domain.search.dto.SearchResponseDTO; import DiffLens.back_end.domain.search.service.interfaces.SearchHistoryService; @@ -30,18 +29,38 @@ public class SearchController { private final SearchRecommendService searchRecommendService; @PostMapping - @Operation(summary = "자연어 검색 ( 자연어 쿼리 직접 입력 ) ( ai 연동 전 )", description = """ - + @Operation(summary = "자연어 검색 ( 자연어 쿼리 직접 입력 ) ( ai 연동 완료, 차트 포함 )", description = """ ## 개요 - 자연어 검색 API 입니다. + 자연어 검색 API 입니다. AI 서버를 통해 검색을 수행하고, 검색 결과에 대한 차트 추천을 받아 반환합니다. ## request body - 검색 모드와 필터 항목들은 노션에 정리하여 올리겠습니다. - 필터에는 Filter Code 를 넣어주세요. ex) 101, 203, 305 ... - ## 응답 - 검색 결과에 개별응답은 포함하지 않았습니다. 개별 응답 데이터 API를 조회해야 합니다. - + ## 응답 구조 + - **summary**: 검색 결과 요약 정보 (총 응답자 수, 평균 연령, 신뢰도 등) + - **applied_filters_summary**: 적용된 필터 목록 + - **main_chart**: 메인 차트 데이터 (amCharts 형식) + - `chart_type`: 차트 타입 (pie, donut, column, bar, map, stacked-bar, infographic 등) + - `metric`: 차트를 생성한 메트릭 (age_group, gender, residence 등) + - `title`: 차트 제목 + - `reasoning`: 차트 선택 이유 (메인 차트에만 제공) + - `data`: 차트 데이터 포인트 배열 + - **sub_charts**: 서브 차트 데이터 배열 (최대 2개, amCharts 형식) + - 메인 차트와 동일한 구조이지만 `reasoning`은 null + + ## 차트 타입 + - **pie**: 원형 차트 (2-5개 카테고리) + - **donut**: 도넛 차트 (4-8개 카테고리) + - **column**: 세로 막대 차트 (8개 이상 카테고리) + - **bar**: 가로 막대 차트 (레이블이 긴 경우) + - **map**: 지도 차트 (지역 데이터) + - **stacked-bar**: 누적 가로 막대 차트 (연령대별 성별 분포 등) + - **infographic**: 인포그래픽 차트 (직업별 성별 비율 등) + + ## 참고사항 + - 검색 결과에 개별응답은 포함하지 않았습니다. 개별 응답 데이터 API를 조회해야 합니다. + - 차트는 AI 서버에서 자동으로 추천되며, 검색 결과의 특성에 따라 최적의 차트 타입이 선택됩니다. """) public ApiResponse naturalLanguage( @RequestBody @Valid SearchRequestDTO.NaturalLanguage request) { @@ -50,20 +69,21 @@ public ApiResponse naturalLanguage( } @PostMapping("/recommended/{recommendedId}") - @Operation(summary = "추천 검색어로 검색 ( ai가 추천해준 검색 정보로 검색 ) ( ai X )", description = """ - + @Operation(summary = "추천 검색어로 검색 ( ai가 추천해준 검색 정보로 검색 ) ( ai 연동 완료, 차트 포함 )", description = """ ## 개요 - AI가 추천해준 검색 정보로 검색합니다. + AI가 추천해준 검색 정보로 검색합니다. 자연어 검색 API와 동일한 응답 구조를 반환하며, 차트도 포함됩니다. ## 요청 - 맞춤 검색 추천 api 호출로 얻은 결과 중 recommendations에 포함된 검색 정보의 id를 recommendedId에 넣어 요청하면 됩니다. - 검색 정보는 DB가 아닌 캐시에 저장되어 일정 시간이 지나면 올바른 recommendedId로 요청해도 오류가 발생합니다. - - 만료되었다는 응답이 발생하면 '맞춤 검색 추천' api를 다시 호출하거나,\n + - 만료되었다는 응답이 발생하면 '맞춤 검색 추천' api를 다시 호출하거나, 추천 검색어 api에서 응답받은 title 혹은 query를 이용해서 자연어 검색 api를 호출하여 검색해주세요. ## 응답 자연어 검색과 동일한 형태의 응답을 보냅니다. - + - **main_chart**: 메인 차트 데이터 (amCharts 형식) + - **sub_charts**: 서브 차트 데이터 배열 (최대 2개, amCharts 형식) + - 자세한 차트 구조는 자연어 검색 API 설명을 참고하세요. """) public ApiResponse recommendedSearch( @PathVariable("recommendedId") Long recommendedId) { @@ -185,12 +205,14 @@ public ApiResponse eachResponsesTest( } @GetMapping("/recommended") - @Operation(summary = "맞춤 검색 추천 ( ai 연동 전 )", description = "유저 온보딩 정보, 검색기록을 토대로 검색어를 추천합니다.") - public ApiResponse refine() { + @Operation(summary = "맞춤 검색 추천 ( ai 연동 완료 )", description = "유저 온보딩 정보, 검색기록을 토대로 검색어를 추천합니다.") + public ApiResponse recommendation() { SearchResponseDTO.Recommends recommendations = searchRecommendService.getRecommendations(); return ApiResponse.onSuccess(recommendations); } + /** ------ 👇 아래는 미제공 API 👇 ------ **/ + @GetMapping("/recommended/test") @Operation(summary = "맞춤 검색 추천 (테스트용)", description = """ ## 개요 @@ -255,6 +277,9 @@ public ApiResponse refine( ## 응답 POST /search API와 동일한 응답 형식입니다. + - **main_chart**: null (테스트용이므로 차트 미포함) + - **sub_charts**: null (테스트용이므로 차트 미포함) + - 실제 API에서는 서브서버로부터 차트 데이터를 받아옵니다. """) public ApiResponse testSearch() { // Summary 생성 @@ -280,74 +305,16 @@ public ApiResponse testSearch() { .displayValue("남성") .build()); - // Pie Chart 생성 (도넛 차트: 차 브랜드 분포) - ChartDTO.Graph pieChart = ChartDTO.Graph.builder() - .chartId("pie_chart_001") - .reason("20대 남성의 차 브랜드 분포를 시각화하기 위해 도넛 차트를 사용했습니다.") - .chartType("PIE") - .title("20대 남성이 타는 차 브랜드 분포") - .xAxis(null) - .yAxis(null) - .dataPoints(List.of( - ChartDTO.DataPoint.builder().label("기아").value(40).build(), - ChartDTO.DataPoint.builder().label("현대자동차").value(35).build(), - ChartDTO.DataPoint.builder().label("테슬라").value(20).build(), - ChartDTO.DataPoint.builder().label("BMW").value(5).build())) - .build(); - - // Chart 1: 직업 분포 (바 차트) - ChartDTO.Graph jobChart = ChartDTO.Graph.builder() - .chartId("bar_chart_001") - .reason("직업 분포를 월별로 비교하기 위해 바 차트를 사용했습니다.") - .chartType("BAR") - .title("직업 분포") - .xAxis("월") - .yAxis("인원수") - .dataPoints(List.of( - ChartDTO.DataPoint.builder().label("Jan").value(9).build(), - ChartDTO.DataPoint.builder().label("Feb").value(10).build(), - ChartDTO.DataPoint.builder().label("Mar").value(8).build(), - ChartDTO.DataPoint.builder().label("Apr").value(8).build(), - ChartDTO.DataPoint.builder().label("May").value(9).build(), - ChartDTO.DataPoint.builder().label("Jun").value(10).build(), - ChartDTO.DataPoint.builder().label("Jul").value(7).build(), - ChartDTO.DataPoint.builder().label("Aug").value(7).build(), - ChartDTO.DataPoint.builder().label("Sep").value(6).build(), - ChartDTO.DataPoint.builder().label("Oct").value(8).build(), - ChartDTO.DataPoint.builder().label("Nov").value(8).build(), - ChartDTO.DataPoint.builder().label("Dec").value(9).build())) - .build(); - - // Chart 2: 월평균 개인소득 (라인 차트) - ChartDTO.Graph incomeChart = ChartDTO.Graph.builder() - .chartId("line_chart_001") - .reason("월평균 개인소득의 추이를 보기 위해 라인 차트를 사용했습니다.") - .chartType("LINE") - .title("월평균 개인소득") - .xAxis("월") - .yAxis("소득 (만원)") - .dataPoints(List.of( - ChartDTO.DataPoint.builder().label("Jan").value(450).build(), - ChartDTO.DataPoint.builder().label("Feb").value(550).build(), - ChartDTO.DataPoint.builder().label("Mar").value(400).build(), - ChartDTO.DataPoint.builder().label("Apr").value(600).build(), - ChartDTO.DataPoint.builder().label("May").value(500).build(), - ChartDTO.DataPoint.builder().label("Jun").value(700).build(), - ChartDTO.DataPoint.builder().label("Jul").value(650).build(), - ChartDTO.DataPoint.builder().label("Aug").value(750).build(), - ChartDTO.DataPoint.builder().label("Sep").value(700).build(), - ChartDTO.DataPoint.builder().label("Oct").value(800).build(), - ChartDTO.DataPoint.builder().label("Nov").value(900).build(), - ChartDTO.DataPoint.builder().label("Dec").value(1050).build())) - .build(); + // 차트 데이터는 실제 API에서는 서브서버로부터 받아옵니다. + // 테스트용으로 null 처리 // SearchResult 생성 SearchResponseDTO.SearchResult result = SearchResponseDTO.SearchResult.builder() .searchId(999L) // 테스트용 ID .summary(summary) .appliedFiltersSummary(appliedFilters) - .pie(pieChart) - .charts(List.of(jobChart, incomeChart)) + .mainChart(null) // 차트는 서브서버에서 받아옴 + .subCharts(null) .build(); return ApiResponse.onSuccess(result); diff --git a/src/main/java/DiffLens/back_end/domain/search/converter/SummaryDtoConverter.java b/src/main/java/DiffLens/back_end/domain/search/converter/SummaryDtoConverter.java index acf3201..5dc8d8a 100644 --- a/src/main/java/DiffLens/back_end/domain/search/converter/SummaryDtoConverter.java +++ b/src/main/java/DiffLens/back_end/domain/search/converter/SummaryDtoConverter.java @@ -22,10 +22,26 @@ public SearchResponseDTO.SearchResult.Summary requestToDto(MainSearchResponse re .averageAge(getAgeAvg(response)) .dataCaptureDate(getCurrentDate()) .confidenceLevel(null) + .confidenceLevel(getConfidencePercent(response)) // .confidenceLevel(response.getAccuracy() != null ? response.getAccuracy().intValue() : null) .build(); } + private int getConfidencePercent(MainSearchResponse response) { + List panels = response.getPanels(); + if (panels.isEmpty()) return 0; + + double sum = panels.stream() + .mapToDouble(MainSearchResponse.PanelInfo::getSimilarity) + .sum(); + + double avg = sum / panels.size(); + + return (int) Math.round(avg * 100); // 소수점 반올림 후 int로 변환 + } + + + private Double getAgeAvg(MainSearchResponse response) { List panels = response.getPanels(); diff --git a/src/main/java/DiffLens/back_end/domain/search/dto/SearchResponseDTO.java b/src/main/java/DiffLens/back_end/domain/search/dto/SearchResponseDTO.java index f283541..3171be1 100644 --- a/src/main/java/DiffLens/back_end/domain/search/dto/SearchResponseDTO.java +++ b/src/main/java/DiffLens/back_end/domain/search/dto/SearchResponseDTO.java @@ -27,12 +27,14 @@ public static class SearchResult { @JsonProperty("applied_filters_summary") private List appliedFiltersSummary; - private ChartDTO.Graph pie; + @JsonProperty("main_chart") + private ChartData mainChart; - private List charts; + @JsonProperty("sub_charts") + private List subCharts; -// @JsonProperty("panel_data") // 개별 API로 분리 -// private SearchPanelDTO.PanelData panelData; + // @JsonProperty("panel_data") // 개별 API로 분리 + // private SearchPanelDTO.PanelData panelData; // 중간배열 @Getter @@ -65,6 +67,48 @@ public static class AppliedFilter { private String displayValue; } + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ChartData { + @JsonProperty("chart_type") + private String chartType; + + private String metric; + + private String title; + + private String reasoning; + + private List data; + } + + @Getter + @Builder + @AllArgsConstructor + @NoArgsConstructor + public static class ChartDataPoint { + private String category; + + private Integer value; + + // stacked-bar, infographic 차트용 추가 필드들 + private Integer male; + + @JsonProperty("male_max") + private Integer maleMax; + + private Integer female; + + @JsonProperty("female_max") + private Integer femaleMax; + + // map 차트용 + private String id; + + private String name; + } } @@ -86,7 +130,7 @@ public static class EachResponses { @Builder @AllArgsConstructor @NoArgsConstructor - public static class ResponseValues{ + public static class ResponseValues { @JsonProperty("respondent_id") private String respondentId; private String gender; @@ -116,7 +160,7 @@ public static ResponseValues fromPanelDTO(PanelWithRawDataDTO panel, String conc @Builder @AllArgsConstructor @NoArgsConstructor - public static class Recommends{ + public static class Recommends { private List recommendations; } @@ -124,13 +168,10 @@ public static class Recommends{ @Builder @AllArgsConstructor @NoArgsConstructor - public static class Recommend{ + public static class Recommend { private Long id; private String title; private String description; } - - - } diff --git a/src/main/java/DiffLens/back_end/domain/search/repository/cache/RedisRecommendationCacheRepository.java b/src/main/java/DiffLens/back_end/domain/search/repository/cache/RedisRecommendationCacheRepository.java index 8e4c25a..cc5166d 100644 --- a/src/main/java/DiffLens/back_end/domain/search/repository/cache/RedisRecommendationCacheRepository.java +++ b/src/main/java/DiffLens/back_end/domain/search/repository/cache/RedisRecommendationCacheRepository.java @@ -4,7 +4,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; -import java.time.Duration; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; /** @@ -14,12 +14,13 @@ @RequiredArgsConstructor public class RedisRecommendationCacheRepository { + private final static TimeUnit timeUnit = TimeUnit.MINUTES; + private final RedisTemplate redisTemplate; - private static final Duration TTL = Duration.ofMinutes(10); // 유효기간 // 저장 - public void save(String key, Object value) { - redisTemplate.opsForValue().set(key, value, TTL); + public void save(String key, Object value, int ttl) { + redisTemplate.opsForValue().set(key, value, ttl, timeUnit); } @SuppressWarnings("unchecked") @@ -27,11 +28,11 @@ public T findByKey(String key){ return (T) redisTemplate.opsForValue().get(key); } - public T findOrElse(String key, Supplier supplier) { + public T findOrElse(String key, Supplier supplier, int ttl) { T value = findByKey(key); if (value == null) { value = supplier.get(); - save(key, value); + save(key, value, ttl); } return value; } diff --git a/src/main/java/DiffLens/back_end/domain/search/service/implement/NaturalSearchService.java b/src/main/java/DiffLens/back_end/domain/search/service/implement/NaturalSearchService.java index 2fa44cb..a9fc767 100644 --- a/src/main/java/DiffLens/back_end/domain/search/service/implement/NaturalSearchService.java +++ b/src/main/java/DiffLens/back_end/domain/search/service/implement/NaturalSearchService.java @@ -5,25 +5,17 @@ import DiffLens.back_end.domain.panel.repository.PanelRepository; import DiffLens.back_end.domain.panel.repository.projection.PanelWithRawDataDTO; import DiffLens.back_end.domain.search.entity.Filter; -import DiffLens.back_end.domain.search.enums.chart.ChartType; import DiffLens.back_end.domain.search.repository.FilterRepository; -import DiffLens.back_end.domain.search.repository.SearchHistoryRepository; import DiffLens.back_end.global.fastapi.FastApiService; -import DiffLens.back_end.global.fastapi.dto.request.FastNaturalLanguageRequestDTO; import DiffLens.back_end.global.fastapi.dto.request.MainSearchRequest; -import DiffLens.back_end.global.fastapi.dto.response.FastNaturalLanguageResponseDTO; +import DiffLens.back_end.global.fastapi.dto.response.FastChartResponseDTO; import DiffLens.back_end.domain.search.converter.SearchDtoConverter; -import DiffLens.back_end.domain.search.dto.ChartDTO; import DiffLens.back_end.domain.search.dto.SearchRequestDTO; import DiffLens.back_end.domain.search.dto.SearchResponseDTO; -import DiffLens.back_end.domain.search.entity.Chart; import DiffLens.back_end.domain.search.entity.SearchHistory; -import DiffLens.back_end.domain.search.service.interfaces.ChartService; import DiffLens.back_end.domain.search.service.interfaces.SearchHistoryService; import DiffLens.back_end.domain.search.service.interfaces.SearchService; import DiffLens.back_end.global.fastapi.dto.response.MainSearchResponse; -import DiffLens.back_end.global.responses.code.status.error.SearchStatus; -import DiffLens.back_end.global.responses.exception.handler.ErrorHandler; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,17 +35,13 @@ public class NaturalSearchService implements SearchService> summaryConverter; private final SearchDtoConverter, SearchHistory> filterConverter; - private final SearchDtoConverter chartConverter; -// private final SearchDtoConverter> panelResponseConverter; private final CurrentUserService currentUserService; - private final SearchHistoryRepository searchHistoryRepository; @Override @Transactional(readOnly = false) @@ -97,32 +85,67 @@ public SearchResponseDTO.SearchResult search(SearchRequestDTO.NaturalLanguage re // SearchResult.AppliedFilter 생성 List appliedFiltersSummary = filterConverter.requestToDto(null, searchHistory); - // 차트 생성 -// List charts = chartService.makeChart(new FastNaturalLanguageResponseDTO.Data(), searchHistory, foundPanels); // TODO : 서브서버에서 차트 받아서 적용하기 -// charts.forEach(searchHistory::addChart); // 연관관계 편의 메서드 호출 + // 차트 추천 API 호출 + FastChartResponseDTO.ChartRecommendationsResponse chartResponse = + fastApiService.getChartRecommendations(searchHistory.getId()); - // 차트 변환 -// Chart pieChart = charts.stream().filter(chart -> chart.getChartType() == ChartType.PIE) // 상단 차트 ( PIE 하나 )를 변환하여 생성 -// .findFirst().orElse(null); -// ChartDTO.Graph pie = chartConverter.requestToDto(null, pieChart); -// -// List graphs = charts.stream() // PIE를 제외한 차트를 변환하여 List 생성 -// .filter(chart -> chart.getChartType() != ChartType.PIE) -// .map(chart -> chartConverter.requestToDto(null, chart)) -// .toList(); - - // 개별 응답 데이터 처리 및 반환 -// SearchPanelDTO.PanelData panelData = panelResponseConverter.requestToDto(response, foundPanels); + // ChartData 변환 + SearchResponseDTO.SearchResult.ChartData mainChart = convertToChartData(chartResponse.getMainChart()); + List subCharts = chartResponse.getSubCharts().stream() + .map(this::convertToChartData) + .toList(); return SearchResponseDTO.SearchResult.builder() .searchId(searchHistory.getId()) .summary(summary) .appliedFiltersSummary(appliedFiltersSummary) - .pie(null) // TODO : fast api에서 받아서 적용하기 - .charts(null) -// .panelData(panelData) // 개별 API로 분리 + .mainChart(mainChart) + .subCharts(subCharts) + .build(); + + } + + /** + * FastAPI ChartData를 SearchResponseDTO ChartData로 변환 + */ + private SearchResponseDTO.SearchResult.ChartData convertToChartData( + FastChartResponseDTO.ChartData fastChartData) { + if (fastChartData == null) { + return null; + } + + List dataPoints = fastChartData.getData().stream() + .map(this::convertToChartDataPoint) + .toList(); + + return SearchResponseDTO.SearchResult.ChartData.builder() + .chartType(fastChartData.getChartType()) + .metric(fastChartData.getMetric()) + .title(fastChartData.getTitle()) + .reasoning(fastChartData.getReasoning()) + .data(dataPoints) .build(); + } + /** + * FastAPI ChartDataPoint를 SearchResponseDTO ChartDataPoint로 변환 + */ + private SearchResponseDTO.SearchResult.ChartDataPoint convertToChartDataPoint( + FastChartResponseDTO.ChartDataPoint fastDataPoint) { + if (fastDataPoint == null) { + return null; + } + + return SearchResponseDTO.SearchResult.ChartDataPoint.builder() + .category(fastDataPoint.getCategory()) + .value(fastDataPoint.getValue()) + .male(fastDataPoint.getMale()) + .maleMax(fastDataPoint.getMaleMax()) + .female(fastDataPoint.getFemale()) + .femaleMax(fastDataPoint.getFemaleMax()) + .id(fastDataPoint.getId()) + .name(fastDataPoint.getName()) + .build(); } // fast api에 보낼 요청 dto 생성 diff --git a/src/main/java/DiffLens/back_end/domain/search/service/implement/SearchRecommendCacheService.java b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendSearchCacheServiceImpl.java similarity index 74% rename from src/main/java/DiffLens/back_end/domain/search/service/implement/SearchRecommendCacheService.java rename to src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendSearchCacheServiceImpl.java index 9979b0e..f9c7547 100644 --- a/src/main/java/DiffLens/back_end/domain/search/service/implement/SearchRecommendCacheService.java +++ b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendSearchCacheServiceImpl.java @@ -2,14 +2,16 @@ import DiffLens.back_end.domain.members.entity.Member; import DiffLens.back_end.domain.search.repository.cache.RedisRecommendationCacheRepository; -import DiffLens.back_end.domain.search.service.interfaces.SearchCacheService; +import DiffLens.back_end.domain.search.service.interfaces.RecommendSearchCacheService; import DiffLens.back_end.global.fastapi.dto.response.FastHomeResponseDTO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @Service @RequiredArgsConstructor -public class SearchRecommendCacheService implements SearchCacheService { +public class RecommendSearchCacheServiceImpl implements RecommendSearchCacheService { + + private static final Integer TTL = 10; // 캐시 정보를 관리하는 repository private final RedisRecommendationCacheRepository cacheRepository; @@ -19,7 +21,7 @@ public class SearchRecommendCacheService implements SearchCacheService { private final CurrentUserService currentUserService; private final SearchService naturalSearchService; - private final SearchCacheService recommendCacheService; + private final RecommendSearchCacheService recommendCacheService; @Override public SearchResponseDTO.SearchResult search(Long recommendedId) { @@ -33,7 +33,7 @@ public SearchResponseDTO.SearchResult search(Long recommendedId) { Member member = currentUserService.getCurrentUser(); // 1. 캐시에 있으면 그거로 SearchRequestDTO.NaturalLanguage 객체 만듦 - FastHomeResponseDTO.Data cacheInfo = recommendCacheService.getCacheInfo(member); + FastHomeResponseDTO.HomeRecommend cacheInfo = recommendCacheService.getCacheInfo(member); if (cacheInfo == null) { // throw new ErrorHandler(SearchStatus.RECOMMENDED_EXPIRED); // 멤버에 해당하는 추천 검색어 캐시가 만료된 경우 } @@ -44,8 +44,6 @@ public SearchResponseDTO.SearchResult search(Long recommendedId) { .findFirst() .orElseThrow(() -> new ErrorHandler(SearchStatus.RECOMMENDED_SEARCH_NOT_FOUND)); // 멤버에 해당하는 캐시 정보는 있지만 recommendedId가 잘못된 경우 - // 2. SearchRequestDTO.NaturalLanguage 이거로 자연어 검색 호출 - naturalSearchService - // 2-1. Recommendation -> SearchRequestDTO.NaturalLanguage 변환 SearchRequestDTO.NaturalLanguage naturalRequestDTO = SearchRequestDTO.NaturalLanguage.builder() .question(recommendation.getQuery()) @@ -53,7 +51,7 @@ public SearchResponseDTO.SearchResult search(Long recommendedId) { .filters(new ArrayList()) // 일단 필터 없이... .build(); - // 2-1. SearchRequestDTO.NaturalLanguage로 기존 자연어 검색 서비스 메서드 호출 -> 반환 + // . SearchRequestDTO.NaturalLanguage로 기존 자연어 검색 서비스 메서드 호출 -> 반환 return naturalSearchService.search(naturalRequestDTO); } diff --git a/src/main/java/DiffLens/back_end/domain/search/service/implement/SearchHistoryServiceImpl.java b/src/main/java/DiffLens/back_end/domain/search/service/implement/SearchHistoryServiceImpl.java index 00c4158..dda093f 100644 --- a/src/main/java/DiffLens/back_end/domain/search/service/implement/SearchHistoryServiceImpl.java +++ b/src/main/java/DiffLens/back_end/domain/search/service/implement/SearchHistoryServiceImpl.java @@ -20,9 +20,11 @@ import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; +import org.springframework.security.core.parameters.P; import org.springframework.stereotype.Service; import java.time.LocalDate; +import java.util.Collections; import java.util.List; @Service @@ -112,7 +114,7 @@ public SearchResponseDTO.EachResponses getEachResponses(Long searchHistoryId, In // Panel 목록 순회하며 응답보낼 데이터 파싱 // panelIds와 concordanceRate의 순서쌍에 맞게 패널 정보와 일치율을 담음 - List values = panelDtoList.stream() + List values = new java.util.ArrayList<>(panelDtoList.stream() .map(panel -> { int index = panelIds.indexOf(panel.getId()); // String rate = (index != -1 && index < concordanceRate.size()) @@ -120,7 +122,10 @@ public SearchResponseDTO.EachResponses getEachResponses(Long searchHistoryId, In return SearchResponseDTO.ResponseValues.fromPanelDTO(panel, rate); }) - .toList(); + .toList()); + + // 유사도 기준 내림차순 정렬 + values.sort( ( a, b ) -> b.getConcordanceRate().compareTo(a.getConcordanceRate()) ); //페이징 정보 생성 ResponsePageDTO.OffsetLimitPageInfo pageInfo = ResponsePageDTO.OffsetLimitPageInfo.from(panelDtoList); diff --git a/src/main/java/DiffLens/back_end/domain/search/service/implement/SearchRecommendServiceImpl.java b/src/main/java/DiffLens/back_end/domain/search/service/implement/SearchRecommendServiceImpl.java index 954ecb1..927a3d0 100644 --- a/src/main/java/DiffLens/back_end/domain/search/service/implement/SearchRecommendServiceImpl.java +++ b/src/main/java/DiffLens/back_end/domain/search/service/implement/SearchRecommendServiceImpl.java @@ -1,16 +1,11 @@ package DiffLens.back_end.domain.search.service.implement; import DiffLens.back_end.domain.members.entity.Member; -import DiffLens.back_end.domain.members.entity.Onboarding; -import DiffLens.back_end.domain.members.enums.Industry; -import DiffLens.back_end.domain.members.enums.Job; -import DiffLens.back_end.domain.members.repository.OnboardingRepository; import DiffLens.back_end.domain.members.service.auth.CurrentUserService; import DiffLens.back_end.domain.search.dto.SearchResponseDTO; -import DiffLens.back_end.domain.search.entity.SearchHistory; -import DiffLens.back_end.domain.search.repository.SearchHistoryRepository; -import DiffLens.back_end.domain.search.service.interfaces.SearchCacheService; +import DiffLens.back_end.domain.search.service.interfaces.RecommendSearchCacheService; import DiffLens.back_end.domain.search.service.interfaces.SearchRecommendService; +import DiffLens.back_end.domain.search.util.member.SearchMemberUtil; import DiffLens.back_end.global.fastapi.FastApiService; import DiffLens.back_end.global.fastapi.dto.request.FastHomeRequestDTO; import DiffLens.back_end.global.fastapi.dto.response.FastHomeResponseDTO; @@ -25,12 +20,11 @@ @RequiredArgsConstructor public class SearchRecommendServiceImpl implements SearchRecommendService { + private final SearchMemberUtil searchMemberUtil; + private final FastApiService fastApiService; private final CurrentUserService currentUserService; - private final SearchCacheService recommendCacheService; - - private final OnboardingRepository onboardingRepository; - private final SearchHistoryRepository searchHistoryRepository; + private final RecommendSearchCacheService recommendCacheService; /** * @@ -49,7 +43,7 @@ public SearchResponseDTO.Recommends getRecommendations() { Member member = currentUserService.getCurrentUser(); // 1. 캐시에 존재하면 캐시에서 꺼내서 반환 - FastHomeResponseDTO.Data cacheInfo = recommendCacheService.getCacheInfo(member); + FastHomeResponseDTO.HomeRecommend cacheInfo = recommendCacheService.getCacheInfo(member); if (cacheInfo != null) { // redis에 값이 있다면 ai 로직을 호출하지 않고 바로 return log.info("[API 호출중] 검색 추천 정보를 캐시에서 조회"); return fastDtoToResponseList(cacheInfo); @@ -57,67 +51,33 @@ public SearchResponseDTO.Recommends getRecommendations() { log.info("[API 호출중] 검색 추천 정보를 조회하기 위해 AI 로직 호출"); - // 2. 온보딩 정보 조회 - Onboarding onboarding = onboardingRepository.findByMember(member) - .orElse(Onboarding.builder() - .job(Job.ETC_FREELANCER) - .industry(Industry.ETC) - .build() - ); // 없으면 그냥 기타 정보 들어있는 객체를 담아서 보냄 - - // 2-1. 온보딩 정보에서 직무(Job)와 직종(Industry) 추출 => HomeRecommendOnboarding 생성 - - FastHomeRequestDTO.HomeRecommendOnboarding onboardingResult = FastHomeRequestDTO.HomeRecommendOnboarding.builder() - .job(onboarding.getJob().getKrValue()) - .industry(onboarding.getIndustry().getKrValue()) - .build(); - - // 3. 검색기록 조회 - List searchHistoryList = searchHistoryRepository.findByMember(member); + // 2. member 기반 요청 데이터 준비 + FastHomeRequestDTO.HomeRecommendRequest fastRequestDTO = searchMemberUtil.makeRequest(member); - // 3-1. 검색기록 목록에서 검색내용 ( content ) 만 추출 - List searchContentList = searchHistoryList.stream() - .map(SearchHistory::getContent) - .toList(); - - // 4. 위에서 구한 것들로 요청 DTO 생성 - FastHomeRequestDTO.HomeRecommendRequest fastRequestDTO = FastHomeRequestDTO.HomeRecommendRequest.builder() - .memberId(member.getId()) - .onboarding(onboardingResult) - .recentSearches(searchContentList) - .build(); - - // 5. fast api 요청 후 받은 응답결과를 저장 + // 3. fast api 요청 후, 응답결과를 저장 FastHomeResponseDTO.HomeRecommend fastResponse = fastApiService.recommend(fastRequestDTO); - // 6. 클라이언트에게 보낼 DTO 생성 후 + // 4. 클라이언트에게 보낼 DTO 생성 후 SearchResponseDTO.Recommends result = fastDtoToResponseList(fastResponse); - recommendCacheService.saveCacheInfo(member, fastResponse.getData()); // 이후에 조회할 때 캐시(Redis)에서 조회하기 위해 캐시에 저장 + recommendCacheService.saveCacheInfo(member, fastResponse); // 이후에 조회할 때 캐시(Redis)에서 조회하기 위해 캐시에 저장 - // 7. 반환 + // 5. 반환 return result; } - // Fast api에서 받은 응답 객체를 클라이언트에게 응답보낼 dto로 변환 - // FastHomeResponseDTO.HomeRecommend -> SearchResponseDTO.Recommends - private SearchResponseDTO.Recommends fastDtoToResponseList(FastHomeResponseDTO.HomeRecommend fastResponse) { - // 응답받은 객체에서 추천 객체만 뽑아냄 - return fastDtoToResponseList(fastResponse.getData()); - } - // Fast api에서 받은 응답 객체를 클라이언트에게 응답보낼 dto로 변환 // FastHomeResponseDTO.Data -> SearchResponseDTO.Recommends - private SearchResponseDTO.Recommends fastDtoToResponseList(FastHomeResponseDTO.Data recommedationData){ + private SearchResponseDTO.Recommends fastDtoToResponseList(FastHomeResponseDTO.HomeRecommend recommendationResponse){ // 응답받은 객체에서 추천 객체만 뽑아냄 - List recommendations = recommedationData.getRecommendations(); + List recommendations = recommendationResponse.getRecommendations(); // 추천 객체제를 순회하며 Recommend( 클라이언트 DTO에 담기는 ) 목록 생성 List recommendList = recommendations.stream() .map(recommendation -> SearchResponseDTO.Recommend.builder() .id(recommendation.getId()) - .title(recommendation.getTitle()) - .description(recommendation.getTitle()) + .title(recommendation.getQuery()) + .description(recommendation.getDescription()) .build() ).toList(); diff --git a/src/main/java/DiffLens/back_end/domain/search/service/interfaces/RecommendSearchCacheService.java b/src/main/java/DiffLens/back_end/domain/search/service/interfaces/RecommendSearchCacheService.java new file mode 100644 index 0000000..92a5c7b --- /dev/null +++ b/src/main/java/DiffLens/back_end/domain/search/service/interfaces/RecommendSearchCacheService.java @@ -0,0 +1,16 @@ +package DiffLens.back_end.domain.search.service.interfaces; + +import DiffLens.back_end.domain.members.entity.Member; +import DiffLens.back_end.global.redis.CacheService; + +/** + * + * 추천 데이터 캐시를 다루는 서비스 interface + * + * @param 캐시로 다룰 데이터 + */ +public interface RecommendSearchCacheService extends CacheService { + + + +} diff --git a/src/main/java/DiffLens/back_end/domain/search/service/interfaces/SearchCacheService.java b/src/main/java/DiffLens/back_end/domain/search/service/interfaces/SearchCacheService.java deleted file mode 100644 index 344534a..0000000 --- a/src/main/java/DiffLens/back_end/domain/search/service/interfaces/SearchCacheService.java +++ /dev/null @@ -1,11 +0,0 @@ -package DiffLens.back_end.domain.search.service.interfaces; - -import DiffLens.back_end.domain.members.entity.Member; - -public interface SearchCacheService { - - T getCacheInfo(Member member); - - void saveCacheInfo(Member member, T cacheInfo); - -} diff --git a/src/main/java/DiffLens/back_end/domain/search/util/member/SearchMemberUtil.java b/src/main/java/DiffLens/back_end/domain/search/util/member/SearchMemberUtil.java new file mode 100644 index 0000000..d2d82b6 --- /dev/null +++ b/src/main/java/DiffLens/back_end/domain/search/util/member/SearchMemberUtil.java @@ -0,0 +1,87 @@ +package DiffLens.back_end.domain.search.util.member; + +import DiffLens.back_end.domain.members.entity.Member; +import DiffLens.back_end.domain.members.entity.Onboarding; +import DiffLens.back_end.domain.members.enums.Industry; +import DiffLens.back_end.domain.search.entity.SearchHistory; +import DiffLens.back_end.domain.search.repository.SearchHistoryRepository; +import DiffLens.back_end.global.fastapi.dto.request.FastHomeRequestDTO; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +@RequiredArgsConstructor +public class SearchMemberUtil { + + private static final int LIMIT = 6; + private static final int HISTORY_LIMIT = 50; + + private final SearchHistoryRepository searchHistoryRepository; + + + /** + * + * 추천 검색에 필요한 요청 DTO 를 생성하여 반환합니다. + * + * 담긴 정보 + * - 검색기록 + * - 직업군 + * - 제한 + * + * + * @param member 현재 로그인한 유저 + * @return Fast API에 요청할 DTO 객체 + */ + public FastHomeRequestDTO.HomeRecommendRequest makeRequest(Member member){ + + // 1. 온보딩 정보 조회 ( 직업군 ) + Industry industry = getIndustry(member); + + // 2. 검색기록 조회 + List searchHistoryList = searchHistoryRepository.findByMember(member); + List searchContentList = searchHistoryList.stream() + .map(SearchHistory::getContent) + .toList(); + + return FastHomeRequestDTO.HomeRecommendRequest.builder() + .recentSearches(searchContentList) + .limit(LIMIT) + .industry(industry.getKrValue()) + .build(); + } + + /** + * + * 유저 정보를 조회해서 추천 검색어 조회 + * + * 담긴 정보 + * - 유저 ID + * - 검색기록 제한 + * - 결과 제한 + * - 직업군 + * + * @param member 현래 로그인한 유저 + * @return Fast API에 요청할 DTO + */ + public FastHomeRequestDTO.HomeRecommendByMemberRequest makeRequestByMember(Member member){ + + return FastHomeRequestDTO.HomeRecommendByMemberRequest.builder() + .memberId(member.getId()) + .limit(LIMIT) + .historyLimit(HISTORY_LIMIT) + .industry(getIndustry(member).getKrValue()) + .build(); + + } + + private static Industry getIndustry(Member member) { + // 1. 온보딩 정보 조회 + Onboarding onboarding = member.getOnboarding(); + Industry industry = onboarding.getIndustry(); + return industry; + } + + +} diff --git a/src/main/java/DiffLens/back_end/global/fastapi/FastApiClient.java b/src/main/java/DiffLens/back_end/global/fastapi/FastApiClient.java index 1b83173..b33a1d2 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/FastApiClient.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/FastApiClient.java @@ -49,4 +49,30 @@ public R sendRequest(FastApiRequestType type, T requestBody) { return block; } + /** + * PathVariable을 사용하는 GET 요청 + * + * @param type fast api에 보낼 요청을 관리하는 enum - FastApiRequestType + * @param requestBody 요청 본문 (null 가능) + * @param pathVariables URI에 포함될 경로 변수들 + * @return fast api 로부터 응답받은 데이터 ( R ) + * @param request body의 클래스 + * @param response body의 클래스 + */ + @LogExecutionTime("서브서버 호출 소요시간") + public R sendRequestWithPathVariable(FastApiRequestType type, T requestBody, Object... pathVariables) { + R block = null; + try { + block = fastApiWebClient.get() + .uri(type.getUri(), pathVariables) + .retrieve() + .bodyToMono((Class) type.getResponseType()) + .block(); + } catch (Exception e) { + throw new ErrorHandler(ErrorStatus.SUB_SERVER_ERROR); + } + + return block; + } + } diff --git a/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java b/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java index 5cd4c42..b155e20 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java @@ -9,15 +9,25 @@ @AllArgsConstructor public enum FastApiRequestType { + // 검색 NATURAL_SEARCH("/ai/search", FastNaturalLanguageRequestDTO.NaturalSearch.class, FastNaturalLanguageResponseDTO.NaturalSearch.class), NATURAL_SEARCH2("/api/search/", MainSearchRequest.class, - MainSearchResponse.class), + MainSearchResponse.class), RE_SEARCH("/api/re-search", FastReSearchRequestDTO.ReSearch.class, FastReSearchResponseDTO.ReSearch.class), - RECOMMENDATIONS("/ai/recommendations", FastHomeRequestDTO.HomeRecommendRequest.class, + + // 추천 + RECOMMENDATIONS("/api/quick-search/recommendations", FastHomeRequestDTO.HomeRecommendRequest.class, + FastHomeResponseDTO.HomeRecommend.class), // 일반 추천 + RECOMMENDATIONS_BY_MEMBER("/api/quick-search/recommendations/by-member", + FastHomeRequestDTO.HomeRecommendByMemberRequest.class, FastHomeResponseDTO.HomeRecommend.class), COMPARE("/ai/compare", FastLibraryRequestDTO.LibraryCompare.class, FastLibraryCompareResponseDTO.CompareResult.class), + + // 차트 + CHART_RECOMMENDATIONS("/api/chart/search-result/{searchId}/recommendations", Void.class, + FastChartResponseDTO.ChartRecommendationsResponse.class), // REFINE_SEARCH("/search/refine", // FastNaturalSearchResponseDTO.SearchResult.class), ; diff --git a/src/main/java/DiffLens/back_end/global/fastapi/FastApiService.java b/src/main/java/DiffLens/back_end/global/fastapi/FastApiService.java index 03c1ca0..c9e663f 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/FastApiService.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/FastApiService.java @@ -8,6 +8,7 @@ import DiffLens.back_end.global.fastapi.dto.response.FastNaturalLanguageResponseDTO; import DiffLens.back_end.global.fastapi.dto.response.FastLibraryCompareResponseDTO; import DiffLens.back_end.global.fastapi.dto.response.MainSearchResponse; +import DiffLens.back_end.global.fastapi.dto.response.FastChartResponseDTO; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -41,4 +42,13 @@ public FastHomeResponseDTO.HomeRecommend recommend(FastHomeRequestDTO.HomeRecomm return fastApiClient.sendRequest(FastApiRequestType.RECOMMENDATIONS, request); } + // 차트 추천 + public FastChartResponseDTO.ChartRecommendationsResponse getChartRecommendations(Long searchId) { + return fastApiClient.sendRequestWithPathVariable( + FastApiRequestType.CHART_RECOMMENDATIONS, + null, + searchId + ); + } + } diff --git a/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastHomeRequestDTO.java b/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastHomeRequestDTO.java index ee726b0..7718409 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastHomeRequestDTO.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastHomeRequestDTO.java @@ -1,9 +1,7 @@ package DiffLens.back_end.global.fastapi.dto.request; -import lombok.AllArgsConstructor; -import lombok.Builder; -import lombok.Getter; -import lombok.NoArgsConstructor; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; import java.util.List; @@ -13,6 +11,7 @@ * Spring Boot -> Fast API 요청 형태 * */ +@RequiredArgsConstructor public class FastHomeRequestDTO { @Getter @@ -20,9 +19,36 @@ public class FastHomeRequestDTO { @NoArgsConstructor @AllArgsConstructor public static class HomeRecommendRequest { - private Long memberId; - private HomeRecommendOnboarding onboarding; + + @JsonProperty("search_history") private List recentSearches; + + @JsonProperty("limit") + private Integer limit; + + @JsonProperty("industry") + private String industry; + + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class HomeRecommendByMemberRequest { + + @JsonProperty("member_id") + private Long memberId; + + @JsonProperty("limit") + private Integer limit; + + @JsonProperty("history_limit") + private Integer historyLimit; + + @JsonProperty("industry") + private String industry; + } @Getter diff --git a/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastChartResponseDTO.java b/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastChartResponseDTO.java new file mode 100644 index 0000000..39d18d6 --- /dev/null +++ b/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastChartResponseDTO.java @@ -0,0 +1,83 @@ +package DiffLens.back_end.global.fastapi.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; + +import java.util.List; +import java.util.Map; + +/** + * FastAPI 차트 추천 응답 DTO + */ +public class FastChartResponseDTO { + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ChartRecommendationsResponse { + @JsonProperty("search_id") + private Long searchId; + + @JsonProperty("panel_count") + private Integer panelCount; + + @JsonProperty("original_query") + private String originalQuery; + + @JsonProperty("cohort_stats") + private Map> cohortStats; + + @JsonProperty("main_chart") + private ChartData mainChart; + + @JsonProperty("sub_charts") + private List subCharts; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ChartData { + @JsonProperty("chart_type") + private String chartType; + + private String metric; + + private String title; + + private String reasoning; + + private List data; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class ChartDataPoint { + private String category; + + private Integer value; + + // stacked-bar, infographic 차트용 추가 필드들 + private Integer male; + + @JsonProperty("male_max") + private Integer maleMax; + + private Integer female; + + @JsonProperty("female_max") + private Integer femaleMax; + + // map 차트용 + private String id; + + private String name; + } +} diff --git a/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastHomeResponseDTO.java b/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastHomeResponseDTO.java index 07266da..423c370 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastHomeResponseDTO.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastHomeResponseDTO.java @@ -1,8 +1,7 @@ package DiffLens.back_end.global.fastapi.dto.response; -import lombok.Builder; -import lombok.Getter; -import lombok.Setter; +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.*; import java.util.List; @@ -10,40 +9,118 @@ public class FastHomeResponseDTO { @Getter @Builder + @AllArgsConstructor + @NoArgsConstructor public static class HomeRecommend{ - private Boolean success; - private Data data; + @JsonProperty("recommendations") + private List recommendations; - } + @JsonProperty("strategy_used") + private String strategyUsed; - @Getter - @Setter - public static class Data{ - private List recommendations; + @JsonProperty("total_count") + private int totalCount; + + @JsonProperty("patterns") + private Object patterns; } @Getter - @Setter - public static class Recommendation{ + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Recommendation { + + @JsonProperty("id") private Long id; - private String title; - private String description; - private String icon; + + @JsonProperty("query") private String query; - private RecommendationFilter filters; + + @JsonProperty("count") + private String count; + + @JsonProperty("description") + private String description; + + @JsonProperty("category") + private String category; + + @JsonProperty("personalized") + private Boolean personalized; + + @JsonProperty("search_params") + private Object searchParams; + + @JsonProperty("recommended_mode") + private String recommendedMode; } @Getter - @Setter - public static class RecommendationFilter{ - private String respondentCount; - private List region; - private List occupation; + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SearchParams { + + @JsonProperty("age_group") + private String ageGroup; + + @JsonProperty("gender") private String gender; - private String martialStatus; - private String reason; + + @JsonProperty("region") + private String region; + + @JsonProperty("marital_status") + private String maritalStatus; + + @JsonProperty("occupation") + private List occupation; + + @JsonProperty("brands") + private List brands; + + @JsonProperty("limit") + private int limit; } + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Patterns { + + @JsonProperty("demographic") + private Demographic demographic; + @JsonProperty("survey_consumption") + private SurveyConsumption surveyConsumption; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class Demographic { + + @JsonProperty("연령대") + private List 연령대; + + @JsonProperty("성별") + private List 성별; + + @JsonProperty("거주지역") + private List 거주지역; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class SurveyConsumption { + + @JsonProperty("OTT개수") + private List ott개수; + } } diff --git a/src/main/java/DiffLens/back_end/global/fastapi/dto/response/MainSearchResponse.java b/src/main/java/DiffLens/back_end/global/fastapi/dto/response/MainSearchResponse.java index 1de14dd..5b8fb5b 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/dto/response/MainSearchResponse.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/dto/response/MainSearchResponse.java @@ -7,6 +7,7 @@ import java.util.List; import java.util.Map; +@ToString @Getter @Setter @NoArgsConstructor @@ -68,6 +69,9 @@ public static class PanelInfo { @Schema(description = "해시태그 목록") private List hashtags; + + @JsonProperty("similarity") + private Double similarity; } } diff --git a/src/main/java/DiffLens/back_end/global/redis/CacheService.java b/src/main/java/DiffLens/back_end/global/redis/CacheService.java new file mode 100644 index 0000000..3d44f50 --- /dev/null +++ b/src/main/java/DiffLens/back_end/global/redis/CacheService.java @@ -0,0 +1,17 @@ +package DiffLens.back_end.global.redis; + +/** + * + * Redis 에서 캐시를 다루는 서비스들의 공통 인터페이스 + * + * @param Redis에서 캐시로 다룰 데이터 + * @param 캐시를 저장하거나 찾는 데에 사용할 데이터 + */ +public interface CacheService { + + T getCacheInfo(R key); + + void saveCacheInfo(R data, T cacheInfo); + + +}