diff --git a/src/main/java/DiffLens/back_end/domain/library/controller/LibraryController.java b/src/main/java/DiffLens/back_end/domain/library/controller/LibraryController.java index d423044..7ec43e3 100644 --- a/src/main/java/DiffLens/back_end/domain/library/controller/LibraryController.java +++ b/src/main/java/DiffLens/back_end/domain/library/controller/LibraryController.java @@ -14,6 +14,8 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.List; + @Tag(name = "라이브러리 API") @RestController @RequestMapping("/libraries") @@ -181,28 +183,144 @@ public ApiResponse compareLibraries( return ApiResponse.onSuccess(result); } -// @PostMapping("/compare/test") -// @Operation(summary = "라이브러리 비교 (테스트용)", description = """ -// ## 개요 -// AI 서버 연동 없이 하드코딩된 테스트 데이터로 라이브러리 비교를 제공하는 API입니다. -// 개발 및 테스트 목적으로 사용됩니다. -// -// ## Request Body -// - libraryId1 : 첫 번째 라이브러리 ID (필수) -// - libraryId2 : 두 번째 라이브러리 ID (필수) -// -// ## 응답 데이터 -// - 하드코딩된 테스트 데이터 반환 -// - 실제 라이브러리 데이터는 사용하지 않음 -// -// ## 권한 -// 인증된 사용자만 접근 가능합니다. -// -// """) -// public ApiResponse compareLibrariesTest( -// @RequestBody @Valid LibraryCompareRequestDTO.Compare request) { -// Member member = currentUserService.getCurrentUser(); -// LibraryCompareResponseDTO.CompareResult result = libraryService.compareLibrariesTest(request, member); -// return ApiResponse.onSuccess(result); -// } + @PostMapping("/compare/test") + @Operation(summary = "라이브러리 비교 (테스트용)", description = """ + ## 개요 + AI 서버 연동 없이 하드코딩된 테스트 데이터로 라이브러리 비교를 제공하는 API입니다. + 개발 및 테스트 목적으로 사용됩니다. + + ## Request Body + - libraryId1 : 첫 번째 라이브러리 ID (필수, 실제로는 사용하지 않음) + - libraryId2 : 두 번째 라이브러리 ID (필수, 실제로는 사용하지 않음) + + ## 응답 데이터 + - 하드코딩된 테스트 데이터 반환 + - 실제 라이브러리 데이터는 사용하지 않음 + + ## 권한 + 인증된 사용자만 접근 가능합니다. + + """) + public ApiResponse compareLibrariesTest( + @RequestBody @Valid LibraryCompareRequestDTO.Compare request) { + // Group A 정보 + LibraryCompareResponseDTO.GroupInfo group1 = LibraryCompareResponseDTO.GroupInfo.builder() + .libraryId(1L) + .libraryName("20대 남성이 타는 차 브랜드 분포") + .summary("20대 남성 소비자의 자동차 브랜드 선호도 분석") + .totalCount(100) + .filters(List.of( + LibraryCompareResponseDTO.Filter.builder() + .key("성별") + .values(List.of("남성")) + .build(), + LibraryCompareResponseDTO.Filter.builder() + .key("연령") + .values(List.of("20-29세")) + .build(), + LibraryCompareResponseDTO.Filter.builder() + .key("차량보유") + .values(List.of("있음")) + .build())) + .color("#4169E1") + .build(); + + // Group B 정보 + LibraryCompareResponseDTO.GroupInfo group2 = LibraryCompareResponseDTO.GroupInfo.builder() + .libraryId(2L) + .libraryName("30대 여성 화장품 구매 패턴") + .summary("30대 여성의 화장품 구매 행동 및 선호 브랜드 분석") + .totalCount(250) + .filters(List.of( + LibraryCompareResponseDTO.Filter.builder() + .key("성별") + .values(List.of("여성")) + .build(), + LibraryCompareResponseDTO.Filter.builder() + .key("연령") + .values(List.of("30-39세")) + .build())) + .color("#FF69B4") + .build(); + + // 주요 특성 (특성 1, 2, 3) + List keyCharacteristics = List.of( + LibraryCompareResponseDTO.KeyCharacteristic.builder() + .characteristic("소비 성향") + .description("A그룹은 실용적 소비 성향이 강하고, B그룹은 프리미엄 브랜드 선호도가 높습니다.") + .group1Percentage(30) + .group2Percentage(70) + .difference(40) + .build(), + LibraryCompareResponseDTO.KeyCharacteristic.builder() + .characteristic("브랜드 충성도") + .description("A그룹은 브랜드 전환율이 높은 반면, B그룹은 특정 브랜드에 대한 충성도가 높습니다.") + .group1Percentage(30) + .group2Percentage(70) + .difference(40) + .build(), + LibraryCompareResponseDTO.KeyCharacteristic.builder() + .characteristic("온라인 구매 선호도") + .description("A그룹은 오프라인 구매를 선호하고, B그룹은 온라인 쇼핑몰과 리뷰를 적극 활용합니다.") + .group1Percentage(30) + .group2Percentage(70) + .difference(40) + .build()); + + // Group A 통계 + LibraryCompareResponseDTO.GroupMetrics group1Metrics = LibraryCompareResponseDTO.GroupMetrics.builder() + .male(100) + .female(0) + .seoul(35) + .gyeonggi(40) + .busan(15) + .regionEtc(10) + .avgAge(23.5) + .avgFamily(3.2) + .avgChildren(0.8) + .ratePossessingCar(72) + .avgPersonalIncome(420) + .avgFamilyIncome(340) + .build(); + + // Group B 통계 + LibraryCompareResponseDTO.GroupMetrics group2Metrics = LibraryCompareResponseDTO.GroupMetrics.builder() + .male(0) + .female(100) + .seoul(35) + .gyeonggi(40) + .busan(15) + .regionEtc(10) + .avgAge(32.7) + .avgFamily(4.2) + .avgChildren(3.4) + .ratePossessingCar(30) + .avgPersonalIncome(340) + .avgFamilyIncome(500) + .build(); + + // Comparisons + LibraryCompareResponseDTO.Comparisons comparisons = LibraryCompareResponseDTO.Comparisons.builder() + .group1(group1Metrics) + .group2(group2Metrics) + .build(); + + // Insights + LibraryCompareResponseDTO.Insights insights = LibraryCompareResponseDTO.Insights.builder() + .difference("A그룹(20대 남성)은 차량 구매에 있어 실용성과 가성비를 중시하며, 국산 브랜드 선호도가 높습니다. 반면 B그룹(30대 여성)은 화장품 구매 시 브랜드 이미지와 품질을 중시하며, 프리미엄 브랜드에 대한 지출이 큽니다. 또한 A그룹은 차량 보유율이 72%로 높은 반면, B그룹은 30%로 낮아 이동 수단에 대한 접근 방식이 다릅니다.") + .common("두 그룹 모두 서울과 경기 지역에 집중되어 있으며, 지역 분포가 거의 동일합니다. 또한 모두 온라인 쇼핑 플랫폼을 적극 활용하며, 소셜미디어를 통한 제품 정보 습득이 활발합니다.") + .implication("A그룹은 차량 마케팅 시 실용성과 경제성을 강조하는 전략이 효과적일 것입니다. B그룹은 화장품 마케팅 시 브랜드 스토리텔링과 프리미엄 경험 제공이 중요합니다. 두 그룹 모두 지역별 맞춤 마케팅 전략이 필요하며, 온라인 채널을 통한 타겟팅이 핵심입니다.") + .build(); + + // 최종 결과 생성 + LibraryCompareResponseDTO.CompareResult result = LibraryCompareResponseDTO.CompareResult.builder() + .group1(group1) + .group2(group2) + .keyCharacteristics(keyCharacteristics) + .comparisons(comparisons) + .insights(insights) + .build(); + + return ApiResponse.onSuccess(result); + } } 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..4bcef56 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,10 +1,12 @@ 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; import DiffLens.back_end.domain.search.service.interfaces.SearchRecommendService; import DiffLens.back_end.domain.search.service.interfaces.SearchService; +import DiffLens.back_end.global.dto.ResponsePageDTO; import DiffLens.back_end.global.responses.exception.ApiResponse; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; @@ -12,95 +14,343 @@ import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; +import java.util.ArrayList; +import java.util.List; + @Tag(name = "검색 API") @RestController @RequestMapping("/search") @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 X )", - description = """ - - ## 개요 - AI가 추천해준 검색 정보로 검색합니다. - - ## 요청 - - 맞춤 검색 추천 api 호출로 얻은 결과 중 recommendations에 포함된 검색 정보의 id를 recommendedId에 넣어 요청하면 됩니다. - - 검색 정보는 DB가 아닌 캐시에 저장되어 일정 시간이 지나면 올바른 recommendedId로 요청해도 오류가 발생합니다. - - 만료되었다는 응답이 발생하면 '맞춤 검색 추천' api를 다시 호출하거나,\n - 추천 검색어 api에서 응답받은 title 혹은 query를 이용해서 자연어 검색 api를 호출하여 검색해주세요. - - ## 응답 - 자연어 검색과 동일한 형태의 응답을 보냅니다. - - """) - public ApiResponse recommendedSearch(@PathVariable("recommendedId") Long recommendedId) { - SearchResponseDTO.SearchResult result = recommendationSearchService.search(recommendedId); - return ApiResponse.onSuccess(result); - } - - @GetMapping("/{searchId}/each-responses") - @Operation(summary = "개별 응답 데이터 ( 완료 )", - description = """ - - ## 개요 - 개별 응답 데이터 조회 API 입니다. - 페이징 문제로 인해 검색 API와 분리하였습니다. - - ## 요청값 - - searchId : 검색결과 ID. 검색 API 에서 받은 식별자 값(searchId)를 넣으면 됩니다. - - page : 페이지 번호입니다. 1부터 시작입니다. - - size : 한 페이지 크기입니다. - - ## 응답 - 현재 피그마에 나와있는대로 구현했습니다. - - """) - public ApiResponse eachResponses(@PathVariable("searchId") Long searchId, @RequestParam("page") Integer page, @RequestParam("size") Integer size){ - SearchResponseDTO.EachResponses result = searchHistoryService.getEachResponses(searchId, page, size); - return ApiResponse.onSuccess(result); - } - - @GetMapping("/recommended") - @Operation(summary = "맞춤 검색 추천 ( ai 연동 전 )", description = "유저 온보딩 정보, 검색기록을 토대로 검색어를 추천합니다.") - public ApiResponse refine() { - SearchResponseDTO.Recommends recommendations = searchRecommendService.getRecommendations(); - return ApiResponse.onSuccess(recommendations); - } - - - @PostMapping("/refine") - @Operation(summary = "기존 검색 결과 기반 재검색 ( 미구현 )", description = "아직 구현 전이지만 아마 자연어 검색과 같은 형태로 반환될 듯 싶습니다.", hidden = true) - public ApiResponse refine(@RequestBody @Valid SearchRequestDTO.ExistingSearchResult request) { - SearchResponseDTO.SearchResult result = new SearchResponseDTO.SearchResult(); // 임시 result - return ApiResponse.onSuccess(result); - } + 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 X )", description = """ + + ## 개요 + AI가 추천해준 검색 정보로 검색합니다. + + ## 요청 + - 맞춤 검색 추천 api 호출로 얻은 결과 중 recommendations에 포함된 검색 정보의 id를 recommendedId에 넣어 요청하면 됩니다. + - 검색 정보는 DB가 아닌 캐시에 저장되어 일정 시간이 지나면 올바른 recommendedId로 요청해도 오류가 발생합니다. + - 만료되었다는 응답이 발생하면 '맞춤 검색 추천' api를 다시 호출하거나,\n + 추천 검색어 api에서 응답받은 title 혹은 query를 이용해서 자연어 검색 api를 호출하여 검색해주세요. + + ## 응답 + 자연어 검색과 동일한 형태의 응답을 보냅니다. + + """) + public ApiResponse recommendedSearch( + @PathVariable("recommendedId") Long recommendedId) { + SearchResponseDTO.SearchResult result = recommendationSearchService.search(recommendedId); + return ApiResponse.onSuccess(result); + } + + @GetMapping("/{searchId}/each-responses") + @Operation(summary = "개별 응답 데이터 ( 완료 )", description = """ + + ## 개요 + 개별 응답 데이터 조회 API 입니다. + 페이징 문제로 인해 검색 API와 분리하였습니다. + + ## 요청값 + - searchId : 검색결과 ID. 검색 API 에서 받은 식별자 값(searchId)를 넣으면 됩니다. + - page : 페이지 번호입니다. 1부터 시작입니다. + - size : 한 페이지 크기입니다. + + ## 응답 + 현재 피그마에 나와있는대로 구현했습니다. + + """) + public ApiResponse eachResponses(@PathVariable("searchId") Long searchId, + @RequestParam("page") Integer page, @RequestParam("size") Integer size) { + SearchResponseDTO.EachResponses result = searchHistoryService.getEachResponses(searchId, page, size); + return ApiResponse.onSuccess(result); + } + + @GetMapping("/{searchId}/each-responses/test") + @Operation(summary = "개별 응답 데이터 (테스트용)", description = """ + ## 개요 + AI 서버 연동 전 테스트용 API입니다. 실제 DB 조회 없이 하드코딩된 개별 응답 데이터를 반환합니다. + + ## 요청값 + - searchId : 검색결과 ID (실제로는 사용하지 않음) + - page : 페이지 번호입니다. 1부터 시작입니다. + - size : 한 페이지 크기입니다. + + ## 응답 + GET /search/{searchId}/each-responses API와 동일한 응답 형식입니다. + + """) + public ApiResponse eachResponsesTest( + @PathVariable("searchId") Long searchId, + @RequestParam("page") Integer page, + @RequestParam("size") Integer size) { + // keys 정의 (컬럼 헤더) + List keys = List.of( + "응답자ID-respondent_id", + "성별-gender", + "나이-age", + "거주지-residence", + "월소득-personal_income", + "일치율-concordance_rate"); + + // 하드코딩된 응답 데이터 (사진에 나온 데이터) + List values = List.of( + SearchResponseDTO.ResponseValues.builder() + .respondentId("w100010279508856") + .gender("남성") + .age("22") + .residence("서울") + .personalIncome("250만원") + .concordanceRate("98.12") + .build(), + SearchResponseDTO.ResponseValues.builder() + .respondentId("w100010279508856") + .gender("남성") + .age("22") + .residence("서울") + .personalIncome("250만원") + .concordanceRate("98.12") + .build(), + SearchResponseDTO.ResponseValues.builder() + .respondentId("w100010279508856") + .gender("남성") + .age("22") + .residence("서울") + .personalIncome("250만원") + .concordanceRate("98.12") + .build(), + SearchResponseDTO.ResponseValues.builder() + .respondentId("w100010279508856") + .gender("남성") + .age("22") + .residence("서울") + .personalIncome("250만원") + .concordanceRate("98.12") + .build(), + SearchResponseDTO.ResponseValues.builder() + .respondentId("w100010279508856") + .gender("남성") + .age("22") + .residence("서울") + .personalIncome("250만원") + .concordanceRate("98.12") + .build()); + + // 페이징 정보 생성 (첫 페이지 기준) + ResponsePageDTO.OffsetLimitPageInfo pageInfo = ResponsePageDTO.OffsetLimitPageInfo.builder() + .offset((page - 1) * size) + .currentPage(page) + .currentPageCount(values.size()) + .totalPageCount(10) // 사진에 나온 페이지네이션 "< 1 2 3 4 5 ... 10 >" 기준 + .limit(size) + .totalCount(50L) // 대략적인 총 개수 + .hasNext(page < 10) + .hasPrevious(page > 1) + .build(); + + SearchResponseDTO.EachResponses result = SearchResponseDTO.EachResponses.builder() + .keys(keys) + .values(values) + .pageInfo(pageInfo) + .build(); + + return ApiResponse.onSuccess(result); + } + + @GetMapping("/recommended") + @Operation(summary = "맞춤 검색 추천 ( ai 연동 전 )", description = "유저 온보딩 정보, 검색기록을 토대로 검색어를 추천합니다.") + public ApiResponse refine() { + SearchResponseDTO.Recommends recommendations = searchRecommendService.getRecommendations(); + return ApiResponse.onSuccess(recommendations); + } + + @GetMapping("/recommended/test") + @Operation(summary = "맞춤 검색 추천 (테스트용)", description = """ + ## 개요 + AI 서버 연동 전 테스트용 API입니다. 실제 DB 조회나 AI 서버 호출 없이 하드코딩된 추천 검색어를 반환합니다. + + ## 응답 + GET /search/recommended API와 동일한 응답 형식입니다. + """) + public ApiResponse recommendTest() { + // 하드코딩된 추천 검색어 목록 + List recommendations = List.of( + SearchResponseDTO.Recommend.builder() + .id(1L) + .title("20대 남성 100명") + .description("마케터 맞춤 추천") + .build(), + SearchResponseDTO.Recommend.builder() + .id(2L) + .title("서울 거주 주부 300명") + .description("마케터 맞춤 추천") + .build(), + SearchResponseDTO.Recommend.builder() + .id(3L) + .title("40대 기혼 남성 500명") + .description("마케터 맞춤 추천") + .build(), + SearchResponseDTO.Recommend.builder() + .id(4L) + .title("20대 남성 100명") + .description("마케터 맞춤 추천") + .build(), + SearchResponseDTO.Recommend.builder() + .id(5L) + .title("서울 거주 주부 300명") + .description("마케터 맞춤 추천") + .build(), + SearchResponseDTO.Recommend.builder() + .id(6L) + .title("40대 기혼 남성 500명") + .description("마케터 맞춤 추천") + .build()); + + SearchResponseDTO.Recommends result = SearchResponseDTO.Recommends.builder() + .recommendations(recommendations) + .build(); + + return ApiResponse.onSuccess(result); + } + + @PostMapping("/refine") + @Operation(summary = "기존 검색 결과 기반 재검색 ( 미구현 )", description = "아직 구현 전이지만 아마 자연어 검색과 같은 형태로 반환될 듯 싶습니다.", hidden = true) + public ApiResponse refine( + @RequestBody @Valid SearchRequestDTO.ExistingSearchResult request) { + SearchResponseDTO.SearchResult result = new SearchResponseDTO.SearchResult(); // 임시 result + return ApiResponse.onSuccess(result); + } + + @PostMapping("/test") + @Operation(summary = "검색 테스트 API (하드코딩 데이터)", description = """ + ## 개요 + AI 서버 연동 전 테스트용 API입니다. 실제 DB 조회나 AI 서버 호출 없이 하드코딩된 데이터를 반환합니다. + + ## 응답 + POST /search API와 동일한 응답 형식입니다. + """) + public ApiResponse testSearch() { + // Summary 생성 + SearchResponseDTO.SearchResult.Summary summary = SearchResponseDTO.SearchResult.Summary.builder() + .totalRespondents(100) + .averageAge(24.5) + .dataCaptureDate("2024.09.20") + .confidenceLevel(95) + .build(); + + // Applied Filters 생성 + List appliedFilters = new ArrayList<>(); + appliedFilters.add(SearchResponseDTO.SearchResult.AppliedFilter.builder() + .key("respondent_count") + .displayValue("100명") + .build()); + appliedFilters.add(SearchResponseDTO.SearchResult.AppliedFilter.builder() + .key("age") + .displayValue("20-29세") + .build()); + appliedFilters.add(SearchResponseDTO.SearchResult.AppliedFilter.builder() + .key("gender") + .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(); + + // SearchResult 생성 + SearchResponseDTO.SearchResult result = SearchResponseDTO.SearchResult.builder() + .searchId(999L) // 테스트용 ID + .summary(summary) + .appliedFiltersSummary(appliedFilters) + .pie(pieChart) + .charts(List.of(jobChart, incomeChart)) + .build(); + + return ApiResponse.onSuccess(result); + } }