From 7d8bf99e30450a6f91f83e5fc22b2ceaf230e2ca Mon Sep 17 00:00:00 2001 From: junyong Date: Tue, 18 Nov 2025 03:21:45 +0900 Subject: [PATCH 01/10] =?UTF-8?q?feat=20:=20=EA=B2=80=EC=83=89=EA=B2=B0?= =?UTF-8?q?=EA=B3=BC=20=EC=9D=91=EB=8B=B5=EA=B2=B0=EA=B3=BC=20=EC=8B=A0?= =?UTF-8?q?=EB=A2=B0=EB=8F=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/converter/SummaryDtoConverter.java | 16 ++++++++++++++++ .../fastapi/dto/response/MainSearchResponse.java | 4 ++++ 2 files changed, 20 insertions(+) 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/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; } } From 0db85cf4128b248215fef8c850c0a78c4deccb18 Mon Sep 17 00:00:00 2001 From: junyong Date: Tue, 18 Nov 2025 03:26:29 +0900 Subject: [PATCH 02/10] =?UTF-8?q?feat=20:=20=EA=B0=9C=EB=B3=84=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EC=9C=A0=EC=82=AC=EB=8F=84=20=EA=B8=B0=EC=A4=80=20=EB=82=B4?= =?UTF-8?q?=EB=A6=BC=EC=B0=A8=EC=88=9C=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/search/controller/SearchController.java | 2 +- .../service/implement/SearchHistoryServiceImpl.java | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) 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 5d54fce..d7ed211 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 @@ -25,7 +25,7 @@ public class SearchController { private final SearchRecommendService searchRecommendService; @PostMapping - @Operation(summary = "자연어 검색 ( 자연어 쿼리 직접 입력 ) ( ai 연동 전 )", + @Operation(summary = "자연어 검색 ( 자연어 쿼리 직접 입력 ) ( ai 연동 중 )", description = """ ## 개요 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); From a0ab885f162ea8f79eeb943646db2f11cef24942 Mon Sep 17 00:00:00 2001 From: hardwoong Date: Tue, 18 Nov 2025 11:29:20 +0900 Subject: [PATCH 03/10] =?UTF-8?q?refact:=20Chart.js=20->=20amChart=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD(?= =?UTF-8?q?=EB=85=B8=EC=85=98=20=EC=B0=A8=ED=8A=B8=20=EB=AA=85=EC=84=B8?= =?UTF-8?q?=EC=97=90=20=EC=9D=98=EA=B1=B0)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/controller/SearchController.java | 67 +-------------- .../domain/search/dto/SearchResponseDTO.java | 61 ++++++++++--- .../implement/NaturalSearchService.java | 85 ++++++++++++------- .../global/fastapi/FastApiClient.java | 26 ++++++ .../global/fastapi/FastApiRequestType.java | 2 + .../global/fastapi/FastApiService.java | 10 +++ .../dto/response/FastChartResponseDTO.java | 83 ++++++++++++++++++ 7 files changed, 230 insertions(+), 104 deletions(-) create mode 100644 src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastChartResponseDTO.java 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..7fac6dc 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; @@ -280,74 +279,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/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/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/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..99e29bb 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java @@ -18,6 +18,8 @@ public enum FastApiRequestType { 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/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; + } +} From fed08c718c4c04158f75bfb0b7a7393aec670f59 Mon Sep 17 00:00:00 2001 From: junyong Date: Tue, 18 Nov 2025 14:41:34 +0900 Subject: [PATCH 04/10] =?UTF-8?q?feat=20:=20=EA=B2=80=EC=83=89=EC=96=B4=20?= =?UTF-8?q?=EC=B6=94=EC=B2=9C=20ai=20=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/controller/SearchController.java | 9 +- .../implement/RecommendedSearchService.java | 4 +- .../SearchRecommendCacheService.java | 6 +- .../implement/SearchRecommendServiceImpl.java | 70 +++------- .../search/util/member/SearchMemberUtil.java | 87 +++++++++++++ .../global/fastapi/FastApiRequestType.java | 13 +- .../dto/request/FastHomeRequestDTO.java | 38 +++++- .../dto/response/FastHomeResponseDTO.java | 123 ++++++++++++++---- 8 files changed, 253 insertions(+), 97 deletions(-) create mode 100644 src/main/java/DiffLens/back_end/domain/search/util/member/SearchMemberUtil.java 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 d7ed211..a3ee6ed 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 @@ -25,7 +25,7 @@ public class SearchController { private final SearchRecommendService searchRecommendService; @PostMapping - @Operation(summary = "자연어 검색 ( 자연어 쿼리 직접 입력 ) ( ai 연동 중 )", + @Operation(summary = "자연어 검색 ( 자연어 쿼리 직접 입력 ) ( ai 연동 중, 차트 현재 미포함 )", description = """ ## 개요 @@ -45,7 +45,7 @@ public ApiResponse naturalLanguage(@RequestBody } @PostMapping("/recommended/{recommendedId}") - @Operation(summary = "추천 검색어로 검색 ( ai가 추천해준 검색 정보로 검색 ) ( ai X )", + @Operation(summary = "추천 검색어로 검색 ( ai가 추천해준 검색 정보로 검색 ) ( ai 연동 완료 )", description = """ ## 개요 @@ -89,12 +89,13 @@ public ApiResponse eachResponses(@PathVariable( } @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 👇 ------ **/ @PostMapping("/refine") @Operation(summary = "기존 검색 결과 기반 재검색 ( 미구현 )", description = "아직 구현 전이지만 아마 자연어 검색과 같은 형태로 반환될 듯 싶습니다.", hidden = true) diff --git a/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java index e0c28ea..1dcad2d 100644 --- a/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java +++ b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java @@ -24,7 +24,7 @@ public class RecommendedSearchService implements SearchService { private final CurrentUserService currentUserService; private final SearchService naturalSearchService; - private final SearchCacheService recommendCacheService; + private final SearchCacheService 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); // 멤버에 해당하는 추천 검색어 캐시가 만료된 경우 } 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/SearchRecommendCacheService.java index 9979b0e..2dc176e 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/SearchRecommendCacheService.java @@ -9,7 +9,7 @@ @Service @RequiredArgsConstructor -public class SearchRecommendCacheService implements SearchCacheService { +public class SearchRecommendCacheService implements SearchCacheService { // 캐시 정보를 관리하는 repository private final RedisRecommendationCacheRepository cacheRepository; @@ -19,7 +19,7 @@ public class SearchRecommendCacheService implements SearchCacheService recommendCacheService; - - private final OnboardingRepository onboardingRepository; - private final SearchHistoryRepository searchHistoryRepository; + private final SearchCacheService 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/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/FastApiRequestType.java b/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java index 5cd4c42..894beff 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,20 @@ @AllArgsConstructor public enum FastApiRequestType { + // 검색 NATURAL_SEARCH("/ai/search", FastNaturalLanguageRequestDTO.NaturalSearch.class, - FastNaturalLanguageResponseDTO.NaturalSearch.class), + FastNaturalLanguageResponseDTO.NaturalSearch.class), NATURAL_SEARCH2("/api/search/", MainSearchRequest.class, MainSearchResponse.class), RE_SEARCH("/api/re-search", FastReSearchRequestDTO.ReSearch.class, FastReSearchResponseDTO.ReSearch.class), - RECOMMENDATIONS("/ai/recommendations", FastHomeRequestDTO.HomeRecommendRequest.class, - FastHomeResponseDTO.HomeRecommend.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), + FastLibraryCompareResponseDTO.CompareResult.class), // REFINE_SEARCH("/search/refine", // FastNaturalSearchResponseDTO.SearchResult.class), ; 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/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개수; + } } From 40e726a08b5dc966ea5b3e41dcb19472fb37ea08 Mon Sep 17 00:00:00 2001 From: junyong Date: Tue, 18 Nov 2025 15:30:00 +0900 Subject: [PATCH 05/10] =?UTF-8?q?refactor=20:=20redis=20=EC=BA=90=EC=8B=9C?= =?UTF-8?q?=20=EC=A0=95=EB=B3=B4=EB=A5=BC=20=EB=8B=A4=EB=A3=B0=20=EC=9D=B8?= =?UTF-8?q?=ED=84=B0=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=9E=AC=EC=A0=95?= =?UTF-8?q?=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PanelInfoCacheServiceRecommend.java | 14 ++++++++++++++ .../RedisRecommendationCacheRepository.java | 10 ++++------ ...> RecommendSearchRecommendCacheService.java} | 8 +++++--- .../implement/RecommendedSearchService.java | 4 ++-- .../implement/SearchRecommendServiceImpl.java | 4 ++-- .../interfaces/RecommendSearchCacheService.java | 16 ++++++++++++++++ .../service/interfaces/SearchCacheService.java | 11 ----------- .../back_end/global/redis/CacheService.java | 17 +++++++++++++++++ 8 files changed, 60 insertions(+), 24 deletions(-) create mode 100644 src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceRecommend.java rename src/main/java/DiffLens/back_end/domain/search/service/implement/{SearchRecommendCacheService.java => RecommendSearchRecommendCacheService.java} (80%) create mode 100644 src/main/java/DiffLens/back_end/domain/search/service/interfaces/RecommendSearchCacheService.java delete mode 100644 src/main/java/DiffLens/back_end/domain/search/service/interfaces/SearchCacheService.java create mode 100644 src/main/java/DiffLens/back_end/global/redis/CacheService.java diff --git a/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceRecommend.java b/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceRecommend.java new file mode 100644 index 0000000..b26c64f --- /dev/null +++ b/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceRecommend.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 PanelInfoCacheServiceRecommend extends CacheService { + +} 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..e4472cc 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,6 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; -import java.time.Duration; import java.util.function.Supplier; /** @@ -15,11 +14,10 @@ public class RedisRecommendationCacheRepository { 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); } @SuppressWarnings("unchecked") @@ -27,11 +25,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/SearchRecommendCacheService.java b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendSearchRecommendCacheService.java similarity index 80% 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/RecommendSearchRecommendCacheService.java index 2dc176e..296b84c 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/RecommendSearchRecommendCacheService.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 RecommendSearchRecommendCacheService implements RecommendSearchCacheService { + + private static final Integer TTL = 10; // 캐시 정보를 관리하는 repository private final RedisRecommendationCacheRepository cacheRepository; @@ -31,7 +33,7 @@ public FastHomeResponseDTO.HomeRecommend getCacheInfo(Member member) { // 추천정보를 캐시에 저장함 @Override public void saveCacheInfo(Member member, FastHomeResponseDTO.HomeRecommend cacheInfo) { - cacheRepository.save(getKey(member), cacheInfo); + cacheRepository.save(getKey(member), cacheInfo, TTL); } private String getKey(Member member) { diff --git a/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java index 1dcad2d..5652e33 100644 --- a/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java +++ b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java @@ -5,7 +5,7 @@ import DiffLens.back_end.domain.search.dto.SearchRequestDTO; import DiffLens.back_end.domain.search.dto.SearchResponseDTO; import DiffLens.back_end.domain.search.enums.mode.QuestionMode; -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.SearchService; import DiffLens.back_end.global.fastapi.dto.response.FastHomeResponseDTO; import DiffLens.back_end.global.responses.code.status.error.SearchStatus; @@ -24,7 +24,7 @@ public class RecommendedSearchService implements SearchService { private final CurrentUserService currentUserService; private final SearchService naturalSearchService; - private final SearchCacheService recommendCacheService; + private final RecommendSearchCacheService recommendCacheService; @Override public SearchResponseDTO.SearchResult search(Long recommendedId) { 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 b4228a5..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 @@ -3,7 +3,7 @@ import DiffLens.back_end.domain.members.entity.Member; import DiffLens.back_end.domain.members.service.auth.CurrentUserService; import DiffLens.back_end.domain.search.dto.SearchResponseDTO; -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; @@ -24,7 +24,7 @@ public class SearchRecommendServiceImpl implements SearchRecommendService { private final FastApiService fastApiService; private final CurrentUserService currentUserService; - private final SearchCacheService recommendCacheService; + private final RecommendSearchCacheService recommendCacheService; /** * 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/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); + + +} From e9e40844448d2356e4221ee97d6dc633d3468246 Mon Sep 17 00:00:00 2001 From: junyong Date: Tue, 18 Nov 2025 15:48:13 +0900 Subject: [PATCH 06/10] =?UTF-8?q?fix=20:=20=EC=B6=94=EC=B2=9C=20=EA=B2=80?= =?UTF-8?q?=EC=83=89=EC=96=B4=20=EC=A1=B0=ED=9A=8C=20Redis=20TimeUnit=20?= =?UTF-8?q?=EB=AC=B8=EC=A0=9C=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- ...CacheServiceRecommend.java => PanelInfoCacheService.java} | 2 +- .../domain/panel/service/PanelInfoCacheServiceImpl.java | 4 ++++ .../repository/cache/RedisRecommendationCacheRepository.java | 5 ++++- ...acheService.java => RecommendSearchCacheServiceImpl.java} | 2 +- .../search/service/implement/RecommendedSearchService.java | 4 +--- 5 files changed, 11 insertions(+), 6 deletions(-) rename src/main/java/DiffLens/back_end/domain/panel/service/{PanelInfoCacheServiceRecommend.java => PanelInfoCacheService.java} (77%) create mode 100644 src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceImpl.java rename src/main/java/DiffLens/back_end/domain/search/service/implement/{RecommendSearchRecommendCacheService.java => RecommendSearchCacheServiceImpl.java} (92%) diff --git a/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceRecommend.java b/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheService.java similarity index 77% rename from src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceRecommend.java rename to src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheService.java index b26c64f..897ac9a 100644 --- a/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceRecommend.java +++ b/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheService.java @@ -9,6 +9,6 @@ * * @param 캐시로 다룰 데이터 */ -public interface PanelInfoCacheServiceRecommend extends CacheService { +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..b4b1fae --- /dev/null +++ b/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceImpl.java @@ -0,0 +1,4 @@ +package DiffLens.back_end.domain.panel.service; + +public class PanelInfoCacheServiceImpl { +} 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 e4472cc..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,6 +4,7 @@ import org.springframework.data.redis.core.RedisTemplate; import org.springframework.stereotype.Repository; +import java.util.concurrent.TimeUnit; import java.util.function.Supplier; /** @@ -13,11 +14,13 @@ @RequiredArgsConstructor public class RedisRecommendationCacheRepository { + private final static TimeUnit timeUnit = TimeUnit.MINUTES; + private final RedisTemplate redisTemplate; // 저장 public void save(String key, Object value, int ttl) { - redisTemplate.opsForValue().set(key, value, ttl); + redisTemplate.opsForValue().set(key, value, ttl, timeUnit); } @SuppressWarnings("unchecked") diff --git a/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendSearchRecommendCacheService.java b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendSearchCacheServiceImpl.java similarity index 92% rename from src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendSearchRecommendCacheService.java rename to src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendSearchCacheServiceImpl.java index 296b84c..f9c7547 100644 --- a/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendSearchRecommendCacheService.java +++ b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendSearchCacheServiceImpl.java @@ -9,7 +9,7 @@ @Service @RequiredArgsConstructor -public class RecommendSearchRecommendCacheService implements RecommendSearchCacheService { +public class RecommendSearchCacheServiceImpl implements RecommendSearchCacheService { private static final Integer TTL = 10; diff --git a/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java index 5652e33..8629923 100644 --- a/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java +++ b/src/main/java/DiffLens/back_end/domain/search/service/implement/RecommendedSearchService.java @@ -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); } From 08f4b73a04cc728d7b4b9c9daf4a5d8059f26f05 Mon Sep 17 00:00:00 2001 From: junyong Date: Tue, 18 Nov 2025 16:21:45 +0900 Subject: [PATCH 07/10] =?UTF-8?q?feat=20:=20=ED=8C=A8=EB=84=90=20=EC=A0=95?= =?UTF-8?q?=EB=B3=B4=20=EC=BA=90=EC=8B=B1=20=EC=9C=84=ED=95=9C=20=ED=81=B4?= =?UTF-8?q?=EB=9E=98=EC=8A=A4=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/PanelInfoCacheServiceImpl.java | 36 ++++++++++++++++++- 1 file changed, 35 insertions(+), 1 deletion(-) 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 index b4b1fae..4e0e333 100644 --- a/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceImpl.java +++ b/src/main/java/DiffLens/back_end/domain/panel/service/PanelInfoCacheServiceImpl.java @@ -1,4 +1,38 @@ package DiffLens.back_end.domain.panel.service; -public class PanelInfoCacheServiceImpl { +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) { + + } + + + } From 48ff186259400acd24fab1e86dc57ec16e03ed97 Mon Sep 17 00:00:00 2001 From: hardwoong Date: Tue, 18 Nov 2025 17:24:26 +0900 Subject: [PATCH 08/10] =?UTF-8?q?docs:=20=EC=A3=BC=EC=84=9D=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../DiffLens/back_end/global/fastapi/FastApiRequestType.java | 2 ++ 1 file changed, 2 insertions(+) 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 8d2f6ac..b155e20 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java @@ -24,6 +24,8 @@ public enum FastApiRequestType { 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", From b7d3b08459fbe1c1aa96a0414dfba8a38ecdc740 Mon Sep 17 00:00:00 2001 From: hardwoong Date: Tue, 18 Nov 2025 17:24:51 +0900 Subject: [PATCH 09/10] =?UTF-8?q?refact:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EC=B0=A8=ED=8A=B8=EA=B4=80=EB=A0=A8=20=EB=AC=B8=EC=84=9C?= =?UTF-8?q?=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/controller/SearchController.java | 123 +++++++++++------- 1 file changed, 75 insertions(+), 48 deletions(-) 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 6321471..ddbb25a 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 @@ -22,47 +22,71 @@ @RequiredArgsConstructor public class SearchController { - private final SearchService naturalSearchService; - private final SearchService recommendationSearchService; - private final SearchService existingSearchService; - private final SearchHistoryService searchHistoryService; - private final SearchRecommendService searchRecommendService; - - @PostMapping - @Operation(summary = "자연어 검색 ( 자연어 쿼리 직접 입력 ) ( ai 연동 중, 차트 현재 미포함 )", - description = """ - - ## 개요 - 자연어 검색 API 입니다. - - ## request body - - 검색 모드와 필터 항목들은 노션에 정리하여 올리겠습니다. - - 필터에는 Filter Code 를 넣어주세요. ex) 101, 203, 305 ... - - ## 응답 - 검색 결과에 개별응답은 포함하지 않았습니다. 개별 응답 데이터 API를 조회해야 합니다. - - """) - public ApiResponse naturalLanguage(@RequestBody @Valid SearchRequestDTO.NaturalLanguage request) { - SearchResponseDTO.SearchResult result = naturalSearchService.search(request); - return ApiResponse.onSuccess(result); - } - - @PostMapping("/recommended/{recommendedId}") - @Operation(summary = "추천 검색어로 검색 ( ai가 추천해준 검색 정보로 검색 ) ( ai 연동 완료 )", - description = """ - - ## 개요 - AI가 추천해준 검색 정보로 검색합니다. - - ## 요청 - - 맞춤 검색 추천 api 호출로 얻은 결과 중 recommendations에 포함된 검색 정보의 id를 recommendedId에 넣어 요청하면 됩니다. - - 검색 정보는 DB가 아닌 캐시에 저장되어 일정 시간이 지나면 올바른 recommendedId로 요청해도 오류가 발생합니다. - - 만료되었다는 응답이 발생하면 '맞춤 검색 추천' api를 다시 호출하거나,\n - 추천 검색어 api에서 응답받은 title 혹은 query를 이용해서 자연어 검색 api를 호출하여 검색해주세요. - - ## 응답 - 자연어 검색과 동일한 형태의 응답을 보냅니다. + private final SearchService naturalSearchService; + private final SearchService recommendationSearchService; + private final SearchService existingSearchService; + private final SearchHistoryService searchHistoryService; + private final SearchRecommendService searchRecommendService; + + @PostMapping + @Operation(summary = "자연어 검색 ( 자연어 쿼리 직접 입력 ) ( ai 연동 완료, 차트 포함 )", description = """ + + ## 개요 + 자연어 검색 API 입니다. AI 서버를 통해 검색을 수행하고, 검색 결과에 대한 차트 추천을 받아 반환합니다. + + ## request body + - 검색 모드와 필터 항목들은 노션에 정리하여 올리겠습니다. + - 필터에는 Filter Code 를 넣어주세요. ex) 101, 203, 305 ... + + ## 응답 구조 + - **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) { + SearchResponseDTO.SearchResult result = naturalSearchService.search(request); + return ApiResponse.onSuccess(result); + } + + @PostMapping("/recommended/{recommendedId}") + @Operation(summary = "추천 검색어로 검색 ( ai가 추천해준 검색 정보로 검색 ) ( ai 연동 완료, 차트 포함 )", description = """ + + ## 개요 + AI가 추천해준 검색 정보로 검색합니다. 자연어 검색 API와 동일한 응답 구조를 반환하며, 차트도 포함됩니다. + + ## 요청 + - 맞춤 검색 추천 api 호출로 얻은 결과 중 recommendations에 포함된 검색 정보의 id를 recommendedId에 넣어 요청하면 됩니다. + - 검색 정보는 DB가 아닌 캐시에 저장되어 일정 시간이 지나면 올바른 recommendedId로 요청해도 오류가 발생합니다. + - 만료되었다는 응답이 발생하면 '맞춤 검색 추천' api를 다시 호출하거나, + 추천 검색어 api에서 응답받은 title 혹은 query를 이용해서 자연어 검색 api를 호출하여 검색해주세요. + + ## 응답 + 자연어 검색과 동일한 형태의 응답을 보냅니다. + - **main_chart**: 메인 차트 데이터 (amCharts 형식) + - **sub_charts**: 서브 차트 데이터 배열 (최대 2개, amCharts 형식) + - 자세한 차트 구조는 자연어 검색 API 설명을 참고하세요. """) public ApiResponse recommendedSearch( @@ -184,14 +208,14 @@ public ApiResponse eachResponsesTest( return ApiResponse.onSuccess(result); } - @GetMapping("/recommended") - @Operation(summary = "맞춤 검색 추천 ( ai 연동 완료 )", description = "유저 온보딩 정보, 검색기록을 토대로 검색어를 추천합니다.") - public ApiResponse recommendation() { - SearchResponseDTO.Recommends recommendations = searchRecommendService.getRecommendations(); - return ApiResponse.onSuccess(recommendations); - } + @GetMapping("/recommended") + @Operation(summary = "맞춤 검색 추천 ( ai 연동 완료 )", description = "유저 온보딩 정보, 검색기록을 토대로 검색어를 추천합니다.") + public ApiResponse recommendation() { + SearchResponseDTO.Recommends recommendations = searchRecommendService.getRecommendations(); + return ApiResponse.onSuccess(recommendations); + } - /** ------ 👇 아래는 미제공 API 👇 ------ **/ + /** ------ 👇 아래는 미제공 API 👇 ------ **/ @GetMapping("/recommended/test") @Operation(summary = "맞춤 검색 추천 (테스트용)", description = """ @@ -257,6 +281,9 @@ public ApiResponse refine( ## 응답 POST /search API와 동일한 응답 형식입니다. + - **main_chart**: null (테스트용이므로 차트 미포함) + - **sub_charts**: null (테스트용이므로 차트 미포함) + - 실제 API에서는 서브서버로부터 차트 데이터를 받아옵니다. """) public ApiResponse testSearch() { // Summary 생성 From bcb78123f80431f943f628ce54c5743a2afcb9a6 Mon Sep 17 00:00:00 2001 From: hardwoong Date: Tue, 18 Nov 2025 17:30:09 +0900 Subject: [PATCH 10/10] =?UTF-8?q?refact:=20=EC=8A=A4=EC=9B=A8=EA=B1=B0=20?= =?UTF-8?q?=EB=A7=88=ED=81=AC=EB=8B=A4=EC=9A=B4=20=EA=B9=A8=EC=A7=90=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../search/controller/SearchController.java | 62 +++++++++---------- 1 file changed, 29 insertions(+), 33 deletions(-) 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 ddbb25a..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 @@ -30,39 +30,37 @@ public class SearchController { @PostMapping @Operation(summary = "자연어 검색 ( 자연어 쿼리 직접 입력 ) ( ai 연동 완료, 차트 포함 )", description = """ + ## 개요 + 자연어 검색 API 입니다. AI 서버를 통해 검색을 수행하고, 검색 결과에 대한 차트 추천을 받아 반환합니다. - ## 개요 - 자연어 검색 API 입니다. AI 서버를 통해 검색을 수행하고, 검색 결과에 대한 차트 추천을 받아 반환합니다. - - ## request body - - 검색 모드와 필터 항목들은 노션에 정리하여 올리겠습니다. - - 필터에는 Filter Code 를 넣어주세요. ex) 101, 203, 305 ... - - ## 응답 구조 - - **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 서버에서 자동으로 추천되며, 검색 결과의 특성에 따라 최적의 차트 타입이 선택됩니다. + ## request body + - 검색 모드와 필터 항목들은 노션에 정리하여 올리겠습니다. + - 필터에는 Filter Code 를 넣어주세요. ex) 101, 203, 305 ... + ## 응답 구조 + - **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) { @@ -72,7 +70,6 @@ public ApiResponse naturalLanguage( @PostMapping("/recommended/{recommendedId}") @Operation(summary = "추천 검색어로 검색 ( ai가 추천해준 검색 정보로 검색 ) ( ai 연동 완료, 차트 포함 )", description = """ - ## 개요 AI가 추천해준 검색 정보로 검색합니다. 자연어 검색 API와 동일한 응답 구조를 반환하며, 차트도 포함됩니다. @@ -87,7 +84,6 @@ public ApiResponse naturalLanguage( - **main_chart**: 메인 차트 데이터 (amCharts 형식) - **sub_charts**: 서브 차트 데이터 배열 (최대 2개, amCharts 형식) - 자세한 차트 구조는 자연어 검색 API 설명을 참고하세요. - """) public ApiResponse recommendedSearch( @PathVariable("recommendedId") Long recommendedId) {