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
Expand Up @@ -321,4 +321,64 @@ public ApiResponse<LibraryCompareResponseDTO.CompareResult> 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<LibraryResponseDTO.LibraryDashboard> 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<LibraryResponseDTO.LibraryPanels> 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);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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<ChartData> 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<ChartDataPoint> 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<String> keys;

private List<PanelResponseValues> 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;
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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) {
Expand Down Expand Up @@ -647,6 +657,169 @@ private void createLibraryPanels(Library library, List<String> 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<String> 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<LibraryResponseDTO.LibraryDashboard.ChartData> 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<String> 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<PanelWithRawDataDTO> panelDtoList = searchPanelService.getPanelDtoList(panelIds, pageable);

// 6. 페이지 범위 초과 검사
if (pageNum > panelDtoList.getTotalPages() && panelDtoList.getTotalPages() > 0) {
throw new ErrorHandler(ErrorStatus.PAGE_NO_EXCEED);
}

// 7. Panel 목록을 응답 형식으로 변환 (일치율 없음)
List<LibraryResponseDTO.LibraryPanels.PanelResponseValues> 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<LibraryResponseDTO.LibraryDashboard.ChartDataPoint> 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
Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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),
;
Expand Down
Loading
Loading