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 24acf66..b6f8fdc 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 @@ -321,4 +321,64 @@ public ApiResponse compareLibrariesTest return ApiResponse.onSuccess(result); } + + @GetMapping("/{libraryId}/dashboard") + @Operation(summary = "라이브러리 대시보드 조회", description = """ + ## 개요 + 라이브러리 ID를 입력받아 해당 라이브러리의 패널 배열을 조회하고, 서브서버 API를 호출하여 차트 데이터를 반환합니다. + + ## 응답 + - library_id: 라이브러리 ID + - library_name: 라이브러리 이름 + - panel_count: 패널 개수 + - main_chart: 메인 차트 데이터 (amCharts 형식) + - sub_charts: 서브 차트 데이터 배열 (최대 2개, amCharts 형식) + + ## 차트 구조 + - chart_type: 차트 타입 (pie, donut, column, bar, map, stacked-bar, infographic 등) + - metric: 차트를 생성한 메트릭 + - title: 차트 제목 + - reasoning: 차트 선택 이유 (메인 차트에만 제공) + - data: 차트 데이터 포인트 배열 + + ## 권한 + 본인이 생성한 라이브러리만 조회할 수 있습니다. + """) + public ApiResponse getLibraryDashboard( + @PathVariable("libraryId") Long libraryId) { + Member member = currentUserService.getCurrentUser(); + LibraryResponseDTO.LibraryDashboard result = libraryService.getLibraryDashboard(libraryId, member); + return ApiResponse.onSuccess(result); + } + + @GetMapping("/{libraryId}/panels") + @Operation(summary = "라이브러리 패널 목록 조회 (페이징)", description = """ + ## 개요 + 라이브러리 ID와 페이징 정보를 입력받아 해당 라이브러리의 패널 목록을 조회합니다. + + ## 쿼리 파라미터 + - page: 페이지 번호 (1부터 시작, 기본값: 1) + - size: 페이지 크기 (기본값: 20) + + ## 응답 + - keys: 컬럼명 배열 (respondent_id, gender, age, residence, personal_income) + - values: 패널 데이터 배열 + - page_info: 페이징 정보 + + ## 주의사항 + - 일치율(concordance_rate)은 반환하지 않습니다. + - 검색 API와 달리 유사도 정렬을 하지 않습니다. + + ## 권한 + 본인이 생성한 라이브러리만 조회할 수 있습니다. + """) + public ApiResponse getLibraryPanels( + @PathVariable("libraryId") Long libraryId, + @RequestParam(value = "page", defaultValue = "1") Integer page, + @RequestParam(value = "size", defaultValue = "20") Integer size) { + Member member = currentUserService.getCurrentUser(); + LibraryResponseDTO.LibraryPanels result = libraryService.getLibraryPanels(libraryId, page, size, + member); + return ApiResponse.onSuccess(result); + } } diff --git a/src/main/java/DiffLens/back_end/domain/library/dto/LibraryResponseDTO.java b/src/main/java/DiffLens/back_end/domain/library/dto/LibraryResponseDTO.java index 534f809..238c7d7 100644 --- a/src/main/java/DiffLens/back_end/domain/library/dto/LibraryResponseDTO.java +++ b/src/main/java/DiffLens/back_end/domain/library/dto/LibraryResponseDTO.java @@ -245,4 +245,97 @@ public static class ResidenceDistribution { } } + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class LibraryDashboard { + @JsonProperty("library_id") + private Long libraryId; + + @JsonProperty("library_name") + private String libraryName; + + @JsonProperty("panel_count") + private Integer panelCount; + + @JsonProperty("main_chart") + private ChartData mainChart; + + @JsonProperty("sub_charts") + private List subCharts; + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ChartData { + @JsonProperty("chart_type") + private String chartType; + + private String metric; + + private String title; + + private String reasoning; + + private List data; + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class ChartDataPoint { + private String category; + + private Integer value; + + private Integer male; + + @JsonProperty("male_max") + private Integer maleMax; + + private Integer female; + + @JsonProperty("female_max") + private Integer femaleMax; + + private String id; + + private String name; + } + } + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class LibraryPanels { + private List keys; + + private List values; + + @JsonProperty("page_info") + private ResponsePageDTO.OffsetLimitPageInfo pageInfo; + + @Getter + @Builder + @NoArgsConstructor + @AllArgsConstructor + public static class PanelResponseValues { + @JsonProperty("respondent_id") + private String respondentId; + + private String gender; + + private String age; + + private String residence; + + @JsonProperty("personal_income") + private String personalIncome; + } + } + } 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 2f2230c..cfd3b35 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 @@ -22,11 +22,20 @@ import DiffLens.back_end.domain.search.repository.FilterRepository; import DiffLens.back_end.domain.search.repository.SearchFilterRepository; import DiffLens.back_end.domain.search.repository.SearchHistoryRepository; +import DiffLens.back_end.domain.panel.repository.projection.PanelWithRawDataDTO; +import DiffLens.back_end.domain.search.service.interfaces.SearchPanelService; +import DiffLens.back_end.global.dto.ResponsePageDTO; import DiffLens.back_end.global.fastapi.FastApiService; +import DiffLens.back_end.global.fastapi.dto.request.FastLibraryChartRequestDTO; +import DiffLens.back_end.global.fastapi.dto.response.FastChartResponseDTO; +import DiffLens.back_end.global.fastapi.dto.response.FastLibraryChartResponseDTO; 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; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -46,6 +55,7 @@ public class LibraryService { private final FastApiService fastApiService; private final SearchFilterRepository searchFilterRepository; private final FilterRepository filterRepository; + private final SearchPanelService searchPanelService; @Transactional public LibraryCreateResult createLibrary(LibraryRequestDto.Create request, Member member) { @@ -647,6 +657,169 @@ private void createLibraryPanels(Library library, List panelIds) { libraryPanelRepository.saveAll(libraryPanels); } + /** + * 라이브러리 대시보드 조회 (차트 포함) + */ + @Transactional(readOnly = true) + public LibraryResponseDTO.LibraryDashboard getLibraryDashboard(Long libraryId, Member member) { + // 1. 라이브러리 조회 및 권한 검증 + Library library = libraryRepository.findById(libraryId) + .orElseThrow(() -> new ErrorHandler(ErrorStatus.BAD_REQUEST)); + + if (!library.getMember().getId().equals(member.getId())) { + throw new ErrorHandler(ErrorStatus.FORBIDDEN); + } + + // 2. 패널 ID 배열 조회 + List panelIds = library.getPanelIds(); + if (panelIds == null || panelIds.isEmpty()) { + throw new ErrorHandler(ErrorStatus.BAD_REQUEST); + } + + // 3. 서브서버 API 호출 + FastLibraryChartRequestDTO request = FastLibraryChartRequestDTO.builder() + .panelIds(panelIds) + .libraryName(library.getLibraryName()) + .build(); + + FastLibraryChartResponseDTO.LibraryChartResponse chartResponse = fastApiService + .getChartsFromLibrary(request); + + // 4. 차트 데이터 변환 + LibraryResponseDTO.LibraryDashboard.ChartData mainChart = convertToChartData( + chartResponse.getMainChart()); + List subCharts = chartResponse.getSubCharts() + .stream() + .map(this::convertToChartData) + .toList(); + + // 5. 응답 구성 + return LibraryResponseDTO.LibraryDashboard.builder() + .libraryId(library.getId()) + .libraryName(library.getLibraryName()) + .panelCount(panelIds.size()) + .mainChart(mainChart) + .subCharts(subCharts) + .build(); + } + + /** + * 라이브러리 패널 목록 조회 (페이징, 일치율 없음) + */ + @Transactional(readOnly = true) + public LibraryResponseDTO.LibraryPanels getLibraryPanels(Long libraryId, Integer pageNum, Integer size, + Member member) { + // 1. 페이지 번호 예외처리 + if (pageNum < 1) { + throw new ErrorHandler(ErrorStatus.PAGE_NO_INVALID); + } + + // 2. 라이브러리 조회 및 권한 검증 + Library library = libraryRepository.findById(libraryId) + .orElseThrow(() -> new ErrorHandler(ErrorStatus.BAD_REQUEST)); + + if (!library.getMember().getId().equals(member.getId())) { + throw new ErrorHandler(ErrorStatus.FORBIDDEN); + } + + // 3. 패널 ID 배열 조회 + List panelIds = library.getPanelIds(); + if (panelIds == null || panelIds.isEmpty()) { + return LibraryResponseDTO.LibraryPanels.builder() + .keys(List.of("respondent_id", "gender", "age", "residence", + "personal_income")) + .values(List.of()) + .pageInfo(ResponsePageDTO.OffsetLimitPageInfo.builder() + .offset(0) + .currentPage(1) + .currentPageCount(0) + .totalPageCount(0) + .limit(size) + .totalCount(0L) + .hasNext(false) + .hasPrevious(false) + .build()) + .build(); + } + + // 4. 페이징을 위한 Pageable 객체 생성 + Pageable pageable = PageRequest.of(pageNum - 1, size); + + // 5. PanelId 목록을 이용해서 Panel 조회 + Page panelDtoList = searchPanelService.getPanelDtoList(panelIds, pageable); + + // 6. 페이지 범위 초과 검사 + if (pageNum > panelDtoList.getTotalPages() && panelDtoList.getTotalPages() > 0) { + throw new ErrorHandler(ErrorStatus.PAGE_NO_EXCEED); + } + + // 7. Panel 목록을 응답 형식으로 변환 (일치율 없음) + List values = panelDtoList.stream() + .map(panel -> LibraryResponseDTO.LibraryPanels.PanelResponseValues.builder() + .respondentId(panel.getId()) + .gender(panel.getGender() != null ? panel.getGender().getDisplayValue() + : null) + .age(panel.getAge() != null ? panel.getAge().toString() : null) + .residence(panel.getResidence()) + .personalIncome(panel.getPersonalIncome()) + .build()) + .toList(); + + // 8. 페이징 정보 생성 + ResponsePageDTO.OffsetLimitPageInfo pageInfo = ResponsePageDTO.OffsetLimitPageInfo + .from(panelDtoList); + + return LibraryResponseDTO.LibraryPanels.builder() + .keys(List.of("respondent_id", "gender", "age", "residence", "personal_income")) + .values(values) + .pageInfo(pageInfo) + .build(); + } + + /** + * FastAPI ChartData를 LibraryDashboard ChartData로 변환 + */ + private LibraryResponseDTO.LibraryDashboard.ChartData convertToChartData( + FastChartResponseDTO.ChartData fastChartData) { + if (fastChartData == null) { + return null; + } + + List dataPoints = fastChartData.getData() + .stream() + .map(this::convertToChartDataPoint) + .toList(); + + return LibraryResponseDTO.LibraryDashboard.ChartData.builder() + .chartType(fastChartData.getChartType()) + .metric(fastChartData.getMetric()) + .title(fastChartData.getTitle()) + .reasoning(fastChartData.getReasoning()) + .data(dataPoints) + .build(); + } + + /** + * FastAPI ChartDataPoint를 LibraryDashboard ChartDataPoint로 변환 + */ + private LibraryResponseDTO.LibraryDashboard.ChartDataPoint convertToChartDataPoint( + FastChartResponseDTO.ChartDataPoint fastDataPoint) { + if (fastDataPoint == null) { + return null; + } + + return LibraryResponseDTO.LibraryDashboard.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(); + } + // 라이브러리 생성 결과를 담는 내부 클래스 @lombok.Getter @lombok.AllArgsConstructor 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 bdef582..3a329f8 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java @@ -1,6 +1,8 @@ package DiffLens.back_end.global.fastapi; +import DiffLens.back_end.global.fastapi.dto.request.FastLibraryChartRequestDTO; import DiffLens.back_end.global.fastapi.dto.request.*; +import DiffLens.back_end.global.fastapi.dto.response.FastLibraryChartResponseDTO; import DiffLens.back_end.global.fastapi.dto.response.*; import lombok.AllArgsConstructor; import lombok.Getter; @@ -30,6 +32,8 @@ public enum FastApiRequestType { // 차트 CHART_RECOMMENDATIONS("/api/chart/search-result/{searchId}/recommendations", Void.class, FastChartResponseDTO.ChartRecommendationsResponse.class), + CHART_FROM_LIBRARY("/api/chart/from-library", FastLibraryChartRequestDTO.class, + FastLibraryChartResponseDTO.LibraryChartResponse.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 89ec93a..1eea862 100644 --- a/src/main/java/DiffLens/back_end/global/fastapi/FastApiService.java +++ b/src/main/java/DiffLens/back_end/global/fastapi/FastApiService.java @@ -1,10 +1,11 @@ package DiffLens.back_end.global.fastapi; import DiffLens.back_end.global.fastapi.dto.request.FastHomeRequestDTO; +import DiffLens.back_end.global.fastapi.dto.request.FastLibraryChartRequestDTO; import DiffLens.back_end.global.fastapi.dto.request.FastNaturalLanguageRequestDTO; -import DiffLens.back_end.global.fastapi.dto.request.FastLibraryRequestDTO; import DiffLens.back_end.global.fastapi.dto.request.MainSearchRequest; import DiffLens.back_end.global.fastapi.dto.response.FastHomeResponseDTO; +import DiffLens.back_end.global.fastapi.dto.response.FastLibraryChartResponseDTO; 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; @@ -53,4 +54,10 @@ public FastChartResponseDTO.ChartRecommendationsResponse getChartRecommendations searchId); } + // 라이브러리로부터 차트 생성 + public FastLibraryChartResponseDTO.LibraryChartResponse getChartsFromLibrary( + FastLibraryChartRequestDTO request) { + return fastApiClient.sendRequest(FastApiRequestType.CHART_FROM_LIBRARY, request); + } + } diff --git a/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastLibraryChartRequestDTO.java b/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastLibraryChartRequestDTO.java new file mode 100644 index 0000000..12ecdfc --- /dev/null +++ b/src/main/java/DiffLens/back_end/global/fastapi/dto/request/FastLibraryChartRequestDTO.java @@ -0,0 +1,26 @@ +package DiffLens.back_end.global.fastapi.dto.request; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; + +import java.util.List; + +/** + * 라이브러리로부터 차트 생성 요청 DTO + */ +@Getter +@Builder +@NoArgsConstructor +@AllArgsConstructor +public class FastLibraryChartRequestDTO { + + @JsonProperty("panel_ids") + private List panelIds; + + @JsonProperty("library_name") + private String libraryName; +} + diff --git a/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastLibraryChartResponseDTO.java b/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastLibraryChartResponseDTO.java new file mode 100644 index 0000000..d73bbb7 --- /dev/null +++ b/src/main/java/DiffLens/back_end/global/fastapi/dto/response/FastLibraryChartResponseDTO.java @@ -0,0 +1,70 @@ +package DiffLens.back_end.global.fastapi.dto.response; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.Setter; + +import java.util.List; +import java.util.Map; + +/** + * 라이브러리로부터 차트 생성 응답 DTO + */ +public class FastLibraryChartResponseDTO { + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class LibraryChartResponse { + @JsonProperty("library_name") + private String libraryName; + + @JsonProperty("panel_count") + private Integer panelCount; + + @JsonProperty("panels") + private List panels; + + @JsonProperty("cohort_stats") + private Map> cohortStats; + + @JsonProperty("main_chart") + private FastChartResponseDTO.ChartData mainChart; + + @JsonProperty("sub_charts") + private List subCharts; + } + + @Getter + @Setter + @NoArgsConstructor + @AllArgsConstructor + @Builder + public static class PanelInfo { + @JsonProperty("panel_id") + private String panelId; + + private Integer age; + + private String gender; + + private String residence; + + private String occupation; + + @JsonProperty("marital_status") + private String maritalStatus; + + @JsonProperty("phone_brand") + private String phoneBrand; + + @JsonProperty("car_brand") + private String carBrand; + } +} +