Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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 <T> 캐시로 다룰 데이터
*/
public interface PanelInfoCacheService<T> extends CacheService<T, Panel> {

}
Original file line number Diff line number Diff line change
@@ -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<Panel>{

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) {

}



}
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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<SearchResponseDTO.SearchResult> naturalLanguage(
@RequestBody @Valid SearchRequestDTO.NaturalLanguage request) {
Expand All @@ -50,20 +69,21 @@ public ApiResponse<SearchResponseDTO.SearchResult> 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<SearchResponseDTO.SearchResult> recommendedSearch(
@PathVariable("recommendedId") Long recommendedId) {
Expand Down Expand Up @@ -185,12 +205,14 @@ public ApiResponse<SearchResponseDTO.EachResponses> eachResponsesTest(
}

@GetMapping("/recommended")
@Operation(summary = "맞춤 검색 추천 ( ai 연동 )", description = "유저 온보딩 정보, 검색기록을 토대로 검색어를 추천합니다.")
public ApiResponse<SearchResponseDTO.Recommends> refine() {
@Operation(summary = "맞춤 검색 추천 ( ai 연동 완료 )", description = "유저 온보딩 정보, 검색기록을 토대로 검색어를 추천합니다.")
public ApiResponse<SearchResponseDTO.Recommends> recommendation() {
SearchResponseDTO.Recommends recommendations = searchRecommendService.getRecommendations();
return ApiResponse.onSuccess(recommendations);
}

/** ------ 👇 아래는 미제공 API 👇 ------ **/

@GetMapping("/recommended/test")
@Operation(summary = "맞춤 검색 추천 (테스트용)", description = """
## 개요
Expand Down Expand Up @@ -255,6 +277,9 @@ public ApiResponse<SearchResponseDTO.SearchResult> refine(

## 응답
POST /search API와 동일한 응답 형식입니다.
- **main_chart**: null (테스트용이므로 차트 미포함)
- **sub_charts**: null (테스트용이므로 차트 미포함)
- 실제 API에서는 서브서버로부터 차트 데이터를 받아옵니다.
""")
public ApiResponse<SearchResponseDTO.SearchResult> testSearch() {
// Summary 생성
Expand All @@ -280,74 +305,16 @@ public ApiResponse<SearchResponseDTO.SearchResult> 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);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<MainSearchResponse.PanelInfo> 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<MainSearchResponse.PanelInfo> panels = response.getPanels();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ public static class SearchResult {
@JsonProperty("applied_filters_summary")
private List<AppliedFilter> appliedFiltersSummary;

private ChartDTO.Graph pie;
@JsonProperty("main_chart")
private ChartData mainChart;

private List<ChartDTO.Graph> charts;
@JsonProperty("sub_charts")
private List<ChartData> subCharts;

// @JsonProperty("panel_data") // 개별 API로 분리
// private SearchPanelDTO.PanelData panelData;
// @JsonProperty("panel_data") // 개별 API로 분리
// private SearchPanelDTO.PanelData panelData;

// 중간배열
@Getter
Expand Down Expand Up @@ -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<ChartDataPoint> 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;
}

}

Expand All @@ -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;
Expand Down Expand Up @@ -116,21 +160,18 @@ public static ResponseValues fromPanelDTO(PanelWithRawDataDTO panel, String conc
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Recommends{
public static class Recommends {
private List<Recommend> recommendations;
}

@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class Recommend{
public static class Recommend {
private Long id;
private String title;
private String description;
}




}
Loading
Loading