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 7ec43e3..24acf66 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 @@ -222,7 +222,6 @@ public ApiResponse compareLibrariesTest .key("차량보유") .values(List.of("있음")) .build())) - .color("#4169E1") .build(); // Group B 정보 @@ -240,7 +239,6 @@ public ApiResponse compareLibrariesTest .key("연령") .values(List.of("30-39세")) .build())) - .color("#FF69B4") .build(); // 주요 특성 (특성 1, 2, 3) diff --git a/src/main/java/DiffLens/back_end/domain/library/dto/LibraryCompareResponseDTO.java b/src/main/java/DiffLens/back_end/domain/library/dto/LibraryCompareResponseDTO.java index 02353e4..ed9fa6a 100644 --- a/src/main/java/DiffLens/back_end/domain/library/dto/LibraryCompareResponseDTO.java +++ b/src/main/java/DiffLens/back_end/domain/library/dto/LibraryCompareResponseDTO.java @@ -52,9 +52,6 @@ public static class GroupInfo { @JsonProperty("filters") private List filters; - - @JsonProperty("color") - private String color; } @Getter diff --git a/src/main/java/DiffLens/back_end/domain/library/service/LibraryService.java b/src/main/java/DiffLens/back_end/domain/library/service/LibraryService.java index 756f0c8..2f2230c 100644 --- a/src/main/java/DiffLens/back_end/domain/library/service/LibraryService.java +++ b/src/main/java/DiffLens/back_end/domain/library/service/LibraryService.java @@ -23,7 +23,6 @@ import DiffLens.back_end.domain.search.repository.SearchFilterRepository; import DiffLens.back_end.domain.search.repository.SearchHistoryRepository; import DiffLens.back_end.global.fastapi.FastApiService; -import DiffLens.back_end.global.fastapi.dto.request.FastLibraryRequestDTO; import DiffLens.back_end.global.fastapi.dto.response.FastLibraryCompareResponseDTO; import DiffLens.back_end.global.responses.code.status.error.ErrorStatus; import DiffLens.back_end.global.responses.exception.handler.ErrorHandler; @@ -298,7 +297,8 @@ private LibraryResponseDTO.LibraryDetail.Statistics createStatistics(List } @Transactional(readOnly = true) - public LibraryCompareResponseDTO.CompareResult compareLibraries(LibraryCompareRequestDTO.Compare request, Member member) { + public LibraryCompareResponseDTO.CompareResult compareLibraries(LibraryCompareRequestDTO.Compare request, + Member member) { // 1. 요청 검증 - 같은 라이브러리 비교 불가 if (request.getLibraryId1().equals(request.getLibraryId2())) { throw new ErrorHandler(ErrorStatus.BAD_REQUEST); @@ -318,45 +318,186 @@ public LibraryCompareResponseDTO.CompareResult compareLibraries(LibraryCompareRe } // 4. FastAPI 서버로 비교 요청 - FastLibraryRequestDTO.LibraryCompare fastApiRequest = FastLibraryRequestDTO.LibraryCompare.builder() - .libraryId1(library1.getId()) - .libraryId2(library2.getId()) - .panelIds1(library1.getPanelIds()) - .panelIds2(library2.getPanelIds()) - .build(); - FastLibraryCompareResponseDTO.CompareResult fastApiResponse = fastApiService - .compareLibraries(fastApiRequest); + .compareLibraries(library1.getId(), library2.getId()); // 5. 응답 데이터 구성 return LibraryCompareResponseDTO.CompareResult.builder() - .group1(LibraryCompareResponseDTO.GroupInfo.builder() - .libraryId(library1.getId()) - .libraryName(library1.getLibraryName()) -// .summary() - .totalCount(library1.getPanelIds().size()) - .filters(convertFilters(library1)) - .color("#4169E1") - .build()) - .group2(LibraryCompareResponseDTO.GroupInfo.builder() - .libraryId(library2.getId()) - .libraryName(library2.getLibraryName()) -// .summary() - .totalCount(library2.getPanelIds().size()) - .filters(convertFilters(library2)) - .color("#32CD32") - .build()) - .keyCharacteristics(convertKeyCharacteristics(fastApiResponse.getKeyCharacteristics())) - .comparisons(convertComparisons(fastApiResponse.getBasicComparison(), library1, library2)) - .insights(convertInsights(fastApiResponse.getAiInsights())) + .group1(convertGroupInfo(fastApiResponse.getCohort1(), library1)) + .group2(convertGroupInfo(fastApiResponse.getCohort2(), library2)) + .keyCharacteristics(convertCharacteristics(fastApiResponse.getCharacteristics())) + .comparisons(convertBasicInfoComparisons( + fastApiResponse.getBasicInfo(), + fastApiResponse.getRegionDistribution(), + fastApiResponse.getGenderDistribution())) + .insights(convertKeyInsights(fastApiResponse.getKeyInsights())) .build(); } - private LibraryCompareResponseDTO.Comparisons convertComparisons(List comparisons, Library library1, Library library2) { + /** + * CohortBasicInfo를 GroupInfo로 변환 + */ + private LibraryCompareResponseDTO.GroupInfo convertGroupInfo( + FastLibraryCompareResponseDTO.CohortBasicInfo cohortInfo, + Library library) { + return LibraryCompareResponseDTO.GroupInfo.builder() + .libraryId(Long.parseLong(cohortInfo.getCohortId())) + .libraryName(cohortInfo.getCohortName()) + .totalCount(cohortInfo.getPanelCount()) + .filters(convertFilters(library)) + .build(); + } + + /** + * CharacteristicComparison 리스트를 KeyCharacteristic 리스트로 변환 + */ + private List convertCharacteristics( + List characteristics) { + if (characteristics == null || characteristics.isEmpty()) { + return List.of(); + } + return characteristics.stream() + .map(fast -> LibraryCompareResponseDTO.KeyCharacteristic.builder() + .characteristic(fast.getCharacteristic()) + .description(null) // 새 스키마에는 description이 없음 + .group1Percentage(fast.getCohort1Percentage() != null + ? fast.getCohort1Percentage().intValue() + : 0) + .group2Percentage(fast.getCohort2Percentage() != null + ? fast.getCohort2Percentage().intValue() + : 0) + .difference(fast.getDifferencePercentage() != null + ? fast.getDifferencePercentage().intValue() + : 0) + .build()) + .toList(); + } + + /** + * BasicInfoComparison 리스트를 Comparisons로 변환 + * basic_info에서 메트릭별로 값을 추출하여 GroupMetrics 구성 + * region_distribution 데이터를 group1, group2의 지역 필드에 매핑 + * gender_distribution 데이터를 group1, group2의 성별 필드에 매핑 + */ + private LibraryCompareResponseDTO.Comparisons convertBasicInfoComparisons( + List basicInfo, + FastLibraryCompareResponseDTO.RegionDistribution regionDistribution, + FastLibraryCompareResponseDTO.GenderDistribution genderDistribution) { + if (basicInfo == null || basicInfo.isEmpty()) { + return LibraryCompareResponseDTO.Comparisons.builder() + .group1(LibraryCompareResponseDTO.GroupMetrics.builder().build()) + .group2(LibraryCompareResponseDTO.GroupMetrics.builder().build()) + .build(); + } + + // basic_info에서 메트릭별로 값 추출 + LibraryCompareResponseDTO.GroupMetrics.GroupMetricsBuilder group1Builder = LibraryCompareResponseDTO.GroupMetrics + .builder(); + LibraryCompareResponseDTO.GroupMetrics.GroupMetricsBuilder group2Builder = LibraryCompareResponseDTO.GroupMetrics + .builder(); + + for (FastLibraryCompareResponseDTO.BasicInfoComparison info : basicInfo) { + String metricName = info.getMetricName(); + Double cohort1Value = info.getCohort1Value(); + Double cohort2Value = info.getCohort2Value(); + + switch (metricName) { + case "age": + if (cohort1Value != null) + group1Builder.avgAge(cohort1Value); + if (cohort2Value != null) + group2Builder.avgAge(cohort2Value); + break; + case "family_size": + if (cohort1Value != null) + group1Builder.avgFamily(cohort1Value); + if (cohort2Value != null) + group2Builder.avgFamily(cohort2Value); + break; + case "children_count": + if (cohort1Value != null) + group1Builder.avgChildren(cohort1Value); + if (cohort2Value != null) + group2Builder.avgChildren(cohort2Value); + break; + case "personal_income": + if (cohort1Value != null) + group1Builder.avgPersonalIncome(cohort1Value.intValue()); + if (cohort2Value != null) + group2Builder.avgPersonalIncome(cohort2Value.intValue()); + break; + case "household_income": + if (cohort1Value != null) + group1Builder.avgFamilyIncome(cohort1Value.intValue()); + if (cohort2Value != null) + group2Builder.avgFamilyIncome(cohort2Value.intValue()); + break; + case "car_ownership": + if (cohort1Value != null) + group1Builder.ratePossessingCar(cohort1Value.intValue()); + if (cohort2Value != null) + group2Builder.ratePossessingCar(cohort2Value.intValue()); + break; + } + } + + // region_distribution 데이터를 group1, group2의 지역 필드에 매핑 + if (regionDistribution != null) { + // cohort_1 (group1) 지역 데이터 매핑 + if (regionDistribution.getCohort1() != null) { + java.util.Map cohort1Region = regionDistribution.getCohort1(); + group1Builder.seoul(cohort1Region.getOrDefault("서울", 0.0).intValue()); + group1Builder.gyeonggi(cohort1Region.getOrDefault("경기", 0.0).intValue()); + group1Builder.busan(cohort1Region.getOrDefault("부산", 0.0).intValue()); + group1Builder.regionEtc(cohort1Region.getOrDefault("기타", 0.0).intValue()); + } + + // cohort_2 (group2) 지역 데이터 매핑 + if (regionDistribution.getCohort2() != null) { + java.util.Map cohort2Region = regionDistribution.getCohort2(); + group2Builder.seoul(cohort2Region.getOrDefault("서울", 0.0).intValue()); + group2Builder.gyeonggi(cohort2Region.getOrDefault("경기", 0.0).intValue()); + group2Builder.busan(cohort2Region.getOrDefault("부산", 0.0).intValue()); + group2Builder.regionEtc(cohort2Region.getOrDefault("기타", 0.0).intValue()); + } + } + + // gender_distribution 데이터를 group1, group2의 성별 필드에 매핑 + if (genderDistribution != null) { + // cohort_1 (group1) 성별 데이터 매핑 + if (genderDistribution.getCohort1() != null) { + java.util.Map cohort1Gender = genderDistribution.getCohort1(); + group1Builder.male(cohort1Gender.getOrDefault("남성", 0.0).intValue()); + group1Builder.female(cohort1Gender.getOrDefault("여성", 0.0).intValue()); + } + + // cohort_2 (group2) 성별 데이터 매핑 + if (genderDistribution.getCohort2() != null) { + java.util.Map cohort2Gender = genderDistribution.getCohort2(); + group2Builder.male(cohort2Gender.getOrDefault("남성", 0.0).intValue()); + group2Builder.female(cohort2Gender.getOrDefault("여성", 0.0).intValue()); + } + } + return LibraryCompareResponseDTO.Comparisons.builder() - .group1(getGroupMetrics(library1)) - .group2(getGroupMetrics(library2)) - .build(); + .group1(group1Builder.build()) + .group2(group2Builder.build()) + .build(); + } + + /** + * KeyInsights를 Insights로 변환 (nullable 처리) + */ + private LibraryCompareResponseDTO.Insights convertKeyInsights( + FastLibraryCompareResponseDTO.KeyInsights keyInsights) { + if (keyInsights == null) { + return null; + } + return LibraryCompareResponseDTO.Insights.builder() + .difference(keyInsights.getMainDifferences()) + .common(keyInsights.getCommonalities()) + .implication(keyInsights.getImplications()) + .build(); } private LibraryCompareResponseDTO.GroupMetrics getGroupMetrics(Library library) { @@ -364,82 +505,90 @@ private LibraryCompareResponseDTO.GroupMetrics getGroupMetrics(Library library) int total = panels.size(); if (total == 0) { return LibraryCompareResponseDTO.GroupMetrics.builder() - .male(0).female(0) - .seoul(0).gyeonggi(0).busan(0).regionEtc(0) - .ratePossessingCar(0) - .avgAge(0.0) - .avgFamily(0.0) - .avgChildren(0.0) - .avgPersonalIncome(0) - .avgFamilyIncome(0) - .build(); + .male(0).female(0) + .seoul(0).gyeonggi(0).busan(0).regionEtc(0) + .ratePossessingCar(0) + .avgAge(0.0) + .avgFamily(0.0) + .avgChildren(0.0) + .avgPersonalIncome(0) + .avgFamilyIncome(0) + .build(); } - long maleCount = panels.stream().filter(p -> p.getGender() != null && p.getGender().toString().equals(Gender.MALE)).count(); - long femaleCount = panels.stream().filter(p -> p.getGender() != null && p.getGender().toString().equals(Gender.FEMALE)).count(); + long maleCount = panels.stream() + .filter(p -> p.getGender() != null && p.getGender().toString().equals(Gender.MALE)) + .count(); + long femaleCount = panels.stream() + .filter(p -> p.getGender() != null && p.getGender().toString().equals(Gender.FEMALE)) + .count(); long seoul = panels.stream().filter(p -> p.getRegion() != null && p.getRegion().contains("서울")).count(); - long gyeonggi = panels.stream().filter(p -> p.getRegion() != null && p.getRegion().contains("경기")).count(); + long gyeonggi = panels.stream().filter(p -> p.getRegion() != null && p.getRegion().contains("경기")) + .count(); long busan = panels.stream().filter(p -> p.getRegion() != null && p.getRegion().contains("부산")).count(); long regionEtc = total - seoul - gyeonggi - busan; - long carOwners = panels.stream().filter(p -> p.getCarOwnership() != null && p.getCarOwnership().contains("있음")).count(); + long carOwners = panels.stream() + .filter(p -> p.getCarOwnership() != null && p.getCarOwnership().contains("있음")).count(); double avgAge = panels.stream() - .filter(p -> p.getAge() != null) - .mapToInt(Panel::getAge) - .average() - .orElse(0); + .filter(p -> p.getAge() != null) + .mapToInt(Panel::getAge) + .average() + .orElse(0); double avgFamily = panels.stream() - .filter(p -> p.getFamilySize() != null) - .mapToInt(p -> { - try { - return Integer.parseInt(p.getFamilySize()); - } catch (NumberFormatException e) { - return 0; - } - }).average().orElse(0); + .filter(p -> p.getFamilySize() != null) + .mapToInt(p -> { + try { + return Integer.parseInt(p.getFamilySize()); + } catch (NumberFormatException e) { + return 0; + } + }).average().orElse(0); double avgChildren = panels.stream() - .filter(p -> p.getChildrenCount() != null) - .mapToInt(Panel::getChildrenCount) - .average() - .orElse(0); + .filter(p -> p.getChildrenCount() != null) + .mapToInt(Panel::getChildrenCount) + .average() + .orElse(0); double avgPersonalIncome = panels.stream() - .filter(p -> p.getPersonalIncome() != null) - .mapToInt(p -> parseIncome(p.getPersonalIncome())) - .average() - .orElse(0); + .filter(p -> p.getPersonalIncome() != null) + .mapToInt(p -> parseIncome(p.getPersonalIncome())) + .average() + .orElse(0); double avgFamilyIncome = panels.stream() - .filter(p -> p.getHouseholdIncome() != null) - .mapToInt(p -> parseIncome(p.getHouseholdIncome())) - .average() - .orElse(0); + .filter(p -> p.getHouseholdIncome() != null) + .mapToInt(p -> parseIncome(p.getHouseholdIncome())) + .average() + .orElse(0); return LibraryCompareResponseDTO.GroupMetrics.builder() - .male((int) Math.round((double) maleCount / total * 100)) - .female((int) Math.round((double) femaleCount / total * 100)) - .seoul((int) Math.round((double) seoul / total * 100)) - .gyeonggi((int) Math.round((double) gyeonggi / total * 100)) - .busan((int) Math.round((double) busan / total * 100)) - .regionEtc((int) Math.round((double) regionEtc / total * 100)) - .ratePossessingCar((int) Math.round((double) carOwners / total * 100)) - .avgAge(avgAge) - .avgFamily(avgFamily) - .avgChildren(avgChildren) - .avgPersonalIncome((int) avgPersonalIncome) - .avgFamilyIncome((int) avgFamilyIncome) - .build(); + .male((int) Math.round((double) maleCount / total * 100)) + .female((int) Math.round((double) femaleCount / total * 100)) + .seoul((int) Math.round((double) seoul / total * 100)) + .gyeonggi((int) Math.round((double) gyeonggi / total * 100)) + .busan((int) Math.round((double) busan / total * 100)) + .regionEtc((int) Math.round((double) regionEtc / total * 100)) + .ratePossessingCar((int) Math.round((double) carOwners / total * 100)) + .avgAge(avgAge) + .avgFamily(avgFamily) + .avgChildren(avgChildren) + .avgPersonalIncome((int) avgPersonalIncome) + .avgFamilyIncome((int) avgFamilyIncome) + .build(); } // 문자열 -> 숫자 private int parseIncome(String incomeStr) { - if (incomeStr == null) return 0; + if (incomeStr == null) + return 0; String clean = incomeStr.replaceAll("[^0-9]", ""); - if (clean.isEmpty()) return 0; + if (clean.isEmpty()) + return 0; try { return Integer.parseInt(clean); } catch (NumberFormatException e) { @@ -447,36 +596,19 @@ private int parseIncome(String incomeStr) { } } - - private List convertKeyCharacteristics( - List fastApiCharacteristics) { - - int lastIndex = Math.min(fastApiCharacteristics.size(), 3); - - return fastApiCharacteristics.subList(0, lastIndex-1).stream() - .map(fast -> LibraryCompareResponseDTO.KeyCharacteristic.builder() - .characteristic(fast.getCharacteristic()) - .description(fast.getDescription()) - .group1Percentage(fast.getGroup1Percentage()) - .group2Percentage(fast.getGroup2Percentage()) - .difference(fast.getDifference()) - .build()) - .toList(); - } - private List convertFilters(Library library) { - List searchHistoryLibraries = - searchHistoryLibraryRepository.findByLibraryId(library.getId()); + List searchHistoryLibraries = searchHistoryLibraryRepository + .findByLibraryId(library.getId()); List histories = searchHistoryLibraries.stream() - .map(SearchHistoryLibrary::getHistory) - .toList(); + .map(SearchHistoryLibrary::getHistory) + .toList(); List searchFilters = searchFilterRepository.findBySearchHistory(histories); -// Set filterIds = searchFilters.stream() -// .map(SearchFilter::getId) -// .collect(Collectors.toSet()); + // Set filterIds = searchFilters.stream() + // .map(SearchFilter::getId) + // .collect(Collectors.toSet()); Set filterIds = new HashSet<>(); searchFilters.forEach(searchFilter -> filterIds.addAll(searchFilter.getFilters())); @@ -484,20 +616,18 @@ private List convertFilters(Library library) { List filters = filterRepository.findByIds(filterIds); Map> grouped = filters.stream() - .collect(Collectors.groupingBy( - Filter::getType, - Collectors.mapping(Filter::getDisplayValue, Collectors.toList()) - )); + .collect(Collectors.groupingBy( + Filter::getType, + Collectors.mapping(Filter::getDisplayValue, Collectors.toList()))); return grouped.entrySet().stream() - .map(entry -> LibraryCompareResponseDTO.Filter.builder() - .key(entry.getKey()) - .values(entry.getValue()) - .build()) - .toList(); + .map(entry -> LibraryCompareResponseDTO.Filter.builder() + .key(entry.getKey()) + .values(entry.getValue()) + .build()) + .toList(); } - private void createLibraryPanels(Library library, List panelIds) { List panels = panelRepository.findByIdList(panelIds); @@ -517,14 +647,6 @@ private void createLibraryPanels(Library library, List panelIds) { libraryPanelRepository.saveAll(libraryPanels); } - private LibraryCompareResponseDTO.Insights convertInsights(FastLibraryCompareResponseDTO.Insights insights) { - return LibraryCompareResponseDTO.Insights.builder() - .difference(insights.getDifference()) - .common(insights.getCommon()) - .implication(insights.getImplication()) - .build(); - } - // 라이브러리 생성 결과를 담는 내부 클래스 @lombok.Getter @lombok.AllArgsConstructor 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 b33a1d2..ab70538 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/FastApiClient.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/FastApiClient.java @@ -19,7 +19,7 @@ public class FastApiClient { /** * - * @param type fast api에 보낼 요청을 관리하는 enum - FastApiRequestType + * @param type fast api에 보낼 요청을 관리하는 enum - FastApiRequestType * @param requestBody fast api에 요청 보낼 클래스의 타입 * @return fast api 로부터 응답받은 데이터 ( R ) * @param request body의 클래스 @@ -35,14 +35,14 @@ public R sendRequest(FastApiRequestType type, T requestBody) { } R block = null; - try{ + try { block = fastApiWebClient.post() .uri(type.getUri()) .bodyValue(requestBody) .retrieve() .bodyToMono((Class) type.getResponseType()) .block(); - }catch (Exception e){ // fast api 호출 중 에러 발생시 예외 발생 + } catch (Exception e) { // fast api 호출 중 에러 발생시 예외 발생 throw new ErrorHandler(ErrorStatus.SUB_SERVER_ERROR); } @@ -52,8 +52,8 @@ public R sendRequest(FastApiRequestType type, T requestBody) { /** * PathVariable을 사용하는 GET 요청 * - * @param type fast api에 보낼 요청을 관리하는 enum - FastApiRequestType - * @param requestBody 요청 본문 (null 가능) + * @param type fast api에 보낼 요청을 관리하는 enum - FastApiRequestType + * @param requestBody 요청 본문 (null 가능) * @param pathVariables URI에 포함될 경로 변수들 * @return fast api 로부터 응답받은 데이터 ( R ) * @param request body의 클래스 @@ -75,4 +75,53 @@ public R sendRequestWithPathVariable(FastApiRequestType type, T requestBo return block; } + /** + * Query Parameter를 사용하는 POST 요청 + * + * @param type fast api에 보낼 요청을 관리하는 enum - FastApiRequestType + * @param queryParams 쿼리 파라미터 맵 (key-value 쌍) + * @return fast api 로부터 응답받은 데이터 ( R ) + * @param response body의 클래스 + */ + @LogExecutionTime("서브서버 호출 소요시간") + public R sendRequestWithQueryParams(FastApiRequestType type, java.util.Map queryParams) { + R block = null; + try { + block = fastApiWebClient.post() + .uri(uriBuilder -> { + var builder = uriBuilder.path(type.getUri()); + queryParams.forEach((key, value) -> { + if (value != null) { + builder.queryParam(key, value); + } + }); + return builder.build(); + }) + .retrieve() + .bodyToMono((Class) type.getResponseType()) + .block(); + } catch (WebClientResponseException e) { + // 서브서버의 실제 응답 상태 코드와 메시지 로깅 + System.err.println("=== 서브서버 응답 오류 ==="); + System.err.println("URI: " + type.getUri()); + System.err.println("Query Params: " + queryParams); + System.err.println("Status Code: " + e.getStatusCode()); + System.err.println("Response Body: " + e.getResponseBodyAsString()); + System.err.println("Error Message: " + e.getMessage()); + e.printStackTrace(); + throw new ErrorHandler(ErrorStatus.SUB_SERVER_ERROR); + } catch (Exception e) { + // 기타 예외 (네트워크 오류, 타임아웃 등) + System.err.println("=== 서브서버 요청 오류 ==="); + System.err.println("URI: " + type.getUri()); + System.err.println("Query Params: " + queryParams); + System.err.println("Error Type: " + e.getClass().getName()); + System.err.println("Error Message: " + e.getMessage()); + e.printStackTrace(); + 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 b155e20..bdef582 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java @@ -22,7 +22,9 @@ public enum FastApiRequestType { RECOMMENDATIONS_BY_MEMBER("/api/quick-search/recommendations/by-member", FastHomeRequestDTO.HomeRecommendByMemberRequest.class, FastHomeResponseDTO.HomeRecommend.class), - COMPARE("/ai/compare", FastLibraryRequestDTO.LibraryCompare.class, + + // 라이브러리 비교 + COMPARE("/api/cohort-comparison/compare", Void.class, FastLibraryCompareResponseDTO.CompareResult.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 c9e663f..89ec93a 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/FastApiService.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/FastApiService.java @@ -28,13 +28,16 @@ public FastNaturalLanguageResponseDTO.NaturalSearch getNaturalSearch( } // 자연어 검색2 - public MainSearchResponse getMainSearch(MainSearchRequest request){ + public MainSearchResponse getMainSearch(MainSearchRequest request) { return fastApiClient.sendRequest(FastApiRequestType.NATURAL_SEARCH2, request); } // 라이브러리 비교 - public FastLibraryCompareResponseDTO.CompareResult compareLibraries(FastLibraryRequestDTO.LibraryCompare request) { - return fastApiClient.sendRequest(FastApiRequestType.COMPARE, request); + public FastLibraryCompareResponseDTO.CompareResult compareLibraries(Long cohort1Id, Long cohort2Id) { + java.util.Map queryParams = new java.util.HashMap<>(); + queryParams.put("cohort_1_id", cohort1Id); + queryParams.put("cohort_2_id", cohort2Id); + return fastApiClient.sendRequestWithQueryParams(FastApiRequestType.COMPARE, queryParams); } // 추천검색 @@ -47,8 +50,7 @@ public FastChartResponseDTO.ChartRecommendationsResponse getChartRecommendations return fastApiClient.sendRequestWithPathVariable( FastApiRequestType.CHART_RECOMMENDATIONS, null, - searchId - ); + searchId); } } diff --git a/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastLibraryRequestDTO.java b/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastLibraryRequestDTO.java index f8ef73c..d0d996f 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastLibraryRequestDTO.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastLibraryRequestDTO.java @@ -5,8 +5,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.List; - /** * * 라이브러리 비교 @@ -22,8 +20,6 @@ public class FastLibraryRequestDTO { public static class LibraryCompare{ private Long libraryId1; private Long libraryId2; - private List panelIds1; - private List panelIds2; } } diff --git a/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastLibraryCompareResponseDTO.java b/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastLibraryCompareResponseDTO.java index 2cfadfe..fa0ed75 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastLibraryCompareResponseDTO.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastLibraryCompareResponseDTO.java @@ -1,8 +1,8 @@ package DiffLens.back_end.global.fastapi.dto.response; +import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Getter; import lombok.Setter; -import lombok.ToString; import java.util.List; import java.util.Map; @@ -15,51 +15,191 @@ public class FastLibraryCompareResponseDTO { @Getter @Setter - @ToString public static class CompareResult { + @JsonProperty("cohort_1") + private CohortBasicInfo cohort1; - private List keyCharacteristics; -// private List comparisonCharts; - private List basicComparison; - private Insights aiInsights; + @JsonProperty("cohort_2") + private CohortBasicInfo cohort2; + + @JsonProperty("comparisons") + private List comparisons; + + @JsonProperty("basic_info") + private List basicInfo; + + @JsonProperty("characteristics") + private List characteristics; + + @JsonProperty("key_insights") + private KeyInsights keyInsights; // nullable + + @JsonProperty("summary") + private Summary summary; + + @JsonProperty("region_distribution") + private RegionDistribution regionDistribution; // nullable + + @JsonProperty("gender_distribution") + private GenderDistribution genderDistribution; // nullable + } + + @Getter + @Setter + public static class CohortBasicInfo { + @JsonProperty("cohort_id") + private String cohortId; + + @JsonProperty("cohort_name") + private String cohortName; + + @JsonProperty("panel_count") + private Integer panelCount; + + @JsonProperty("created_at") + private String createdAt; // nullable } @Getter @Setter - @ToString - public static class KeyCharacteristic { + public static class MetricComparison { + @JsonProperty("metric_name") + private String metricName; + + @JsonProperty("metric_label") + private String metricLabel; + + @JsonProperty("cohort_1_data") + private Map cohort1Data; + + @JsonProperty("cohort_2_data") + private Map cohort2Data; + + @JsonProperty("cohort_1_percentage") + private Map cohort1Percentage; + + @JsonProperty("cohort_2_percentage") + private Map cohort2Percentage; + + @JsonProperty("statistical_test") + private StatisticalTest statisticalTest; // nullable + } + + @Getter + @Setter + public static class StatisticalTest { + @JsonProperty("test_type") + private String testType; + + @JsonProperty("chi_square") + private Double chiSquare; + + @JsonProperty("p_value") + private Double pValue; + + @JsonProperty("degrees_of_freedom") + private Integer degreesOfFreedom; + + @JsonProperty("is_significant") + private Boolean isSignificant; + + @JsonProperty("interpretation") + private String interpretation; + + @JsonProperty("error") + private String error; // nullable, 에러 발생 시에만 존재 + } + + @Getter + @Setter + public static class BasicInfoComparison { + @JsonProperty("metric_name") + private String metricName; + + @JsonProperty("metric_label") + private String metricLabel; + + @JsonProperty("cohort_1_value") + private Double cohort1Value; // nullable + + @JsonProperty("cohort_2_value") + private Double cohort2Value; // nullable + + @JsonProperty("difference") + private Double difference; // nullable + + @JsonProperty("difference_percentage") + private Double differencePercentage; // nullable + } + + @Getter + @Setter + public static class CharacteristicComparison { + @JsonProperty("characteristic") private String characteristic; - private String description; - private Integer group1Percentage; - private Integer group2Percentage; - private Integer difference; + + @JsonProperty("cohort_1_percentage") + private Double cohort1Percentage; + + @JsonProperty("cohort_2_percentage") + private Double cohort2Percentage; + + @JsonProperty("cohort_1_count") + private Integer cohort1Count; + + @JsonProperty("cohort_2_count") + private Integer cohort2Count; + + @JsonProperty("difference_percentage") + private Double differencePercentage; } -// @Getter -// @Setter -// @ToString -// public static class ComparisonChart { -// private String chartType; -// private String title; -// private Map data; -// private Map options; -// } + @Getter + @Setter + public static class KeyInsights { + @JsonProperty("main_differences") + private String mainDifferences; + + @JsonProperty("commonalities") + private String commonalities; + + @JsonProperty("implications") + private String implications; + } @Getter @Setter - @ToString - public static class BasicComparison { - private String metric; - private String group1Value; - private String group2Value; + public static class Summary { + @JsonProperty("total_metrics") + private Integer totalMetrics; + + @JsonProperty("significant_differences") + private Integer significantDifferences; + + @JsonProperty("comparison_summary") + private String comparisonSummary; + + @JsonProperty("interpretation") + private String interpretation; } @Getter @Setter - public static class Insights { - private String difference; - private String common; - private String implication; + public static class RegionDistribution { + @JsonProperty("cohort_1") + private Map cohort1; // 지역명 → 비율 (%) + + @JsonProperty("cohort_2") + private Map cohort2; // 지역명 → 비율 (%) } + @Getter + @Setter + public static class GenderDistribution { + @JsonProperty("cohort_1") + private Map cohort1; // "남성" → 비율, "여성" → 비율 (%) + + @JsonProperty("cohort_2") + private Map cohort2; // "남성" → 비율, "여성" → 비율 (%) + } }