Skip to content

Commit 34ee9aa

Browse files
authored
Merge pull request #75 from SW-DiffLens/dev
[dev -> main] 2025.11.21 02:48
2 parents 6a5c833 + 554989b commit 34ee9aa

File tree

7 files changed

+434
-1
lines changed

7 files changed

+434
-1
lines changed

src/main/java/DiffLens/back_end/domain/library/controller/LibraryController.java

Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -321,4 +321,64 @@ public ApiResponse<LibraryCompareResponseDTO.CompareResult> compareLibrariesTest
321321

322322
return ApiResponse.onSuccess(result);
323323
}
324+
325+
@GetMapping("/{libraryId}/dashboard")
326+
@Operation(summary = "라이브러리 대시보드 조회", description = """
327+
## 개요
328+
라이브러리 ID를 입력받아 해당 라이브러리의 패널 배열을 조회하고, 서브서버 API를 호출하여 차트 데이터를 반환합니다.
329+
330+
## 응답
331+
- library_id: 라이브러리 ID
332+
- library_name: 라이브러리 이름
333+
- panel_count: 패널 개수
334+
- main_chart: 메인 차트 데이터 (amCharts 형식)
335+
- sub_charts: 서브 차트 데이터 배열 (최대 2개, amCharts 형식)
336+
337+
## 차트 구조
338+
- chart_type: 차트 타입 (pie, donut, column, bar, map, stacked-bar, infographic 등)
339+
- metric: 차트를 생성한 메트릭
340+
- title: 차트 제목
341+
- reasoning: 차트 선택 이유 (메인 차트에만 제공)
342+
- data: 차트 데이터 포인트 배열
343+
344+
## 권한
345+
본인이 생성한 라이브러리만 조회할 수 있습니다.
346+
""")
347+
public ApiResponse<LibraryResponseDTO.LibraryDashboard> getLibraryDashboard(
348+
@PathVariable("libraryId") Long libraryId) {
349+
Member member = currentUserService.getCurrentUser();
350+
LibraryResponseDTO.LibraryDashboard result = libraryService.getLibraryDashboard(libraryId, member);
351+
return ApiResponse.onSuccess(result);
352+
}
353+
354+
@GetMapping("/{libraryId}/panels")
355+
@Operation(summary = "라이브러리 패널 목록 조회 (페이징)", description = """
356+
## 개요
357+
라이브러리 ID와 페이징 정보를 입력받아 해당 라이브러리의 패널 목록을 조회합니다.
358+
359+
## 쿼리 파라미터
360+
- page: 페이지 번호 (1부터 시작, 기본값: 1)
361+
- size: 페이지 크기 (기본값: 20)
362+
363+
## 응답
364+
- keys: 컬럼명 배열 (respondent_id, gender, age, residence, personal_income)
365+
- values: 패널 데이터 배열
366+
- page_info: 페이징 정보
367+
368+
## 주의사항
369+
- 일치율(concordance_rate)은 반환하지 않습니다.
370+
- 검색 API와 달리 유사도 정렬을 하지 않습니다.
371+
372+
## 권한
373+
본인이 생성한 라이브러리만 조회할 수 있습니다.
374+
""")
375+
public ApiResponse<LibraryResponseDTO.LibraryPanels> getLibraryPanels(
376+
@PathVariable("libraryId") Long libraryId,
377+
@RequestParam(value = "page", defaultValue = "1") Integer page,
378+
@RequestParam(value = "size", defaultValue = "20") Integer size) {
379+
Member member = currentUserService.getCurrentUser();
380+
LibraryResponseDTO.LibraryPanels result = libraryService.getLibraryPanels(libraryId, page, size,
381+
member);
382+
return ApiResponse.onSuccess(result);
383+
}
324384
}

src/main/java/DiffLens/back_end/domain/library/dto/LibraryResponseDTO.java

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -245,4 +245,97 @@ public static class ResidenceDistribution {
245245
}
246246
}
247247

248+
@Getter
249+
@Builder
250+
@NoArgsConstructor
251+
@AllArgsConstructor
252+
public static class LibraryDashboard {
253+
@JsonProperty("library_id")
254+
private Long libraryId;
255+
256+
@JsonProperty("library_name")
257+
private String libraryName;
258+
259+
@JsonProperty("panel_count")
260+
private Integer panelCount;
261+
262+
@JsonProperty("main_chart")
263+
private ChartData mainChart;
264+
265+
@JsonProperty("sub_charts")
266+
private List<ChartData> subCharts;
267+
268+
@Getter
269+
@Builder
270+
@NoArgsConstructor
271+
@AllArgsConstructor
272+
public static class ChartData {
273+
@JsonProperty("chart_type")
274+
private String chartType;
275+
276+
private String metric;
277+
278+
private String title;
279+
280+
private String reasoning;
281+
282+
private List<ChartDataPoint> data;
283+
}
284+
285+
@Getter
286+
@Builder
287+
@NoArgsConstructor
288+
@AllArgsConstructor
289+
public static class ChartDataPoint {
290+
private String category;
291+
292+
private Integer value;
293+
294+
private Integer male;
295+
296+
@JsonProperty("male_max")
297+
private Integer maleMax;
298+
299+
private Integer female;
300+
301+
@JsonProperty("female_max")
302+
private Integer femaleMax;
303+
304+
private String id;
305+
306+
private String name;
307+
}
308+
}
309+
310+
@Getter
311+
@Builder
312+
@NoArgsConstructor
313+
@AllArgsConstructor
314+
public static class LibraryPanels {
315+
private List<String> keys;
316+
317+
private List<PanelResponseValues> values;
318+
319+
@JsonProperty("page_info")
320+
private ResponsePageDTO.OffsetLimitPageInfo pageInfo;
321+
322+
@Getter
323+
@Builder
324+
@NoArgsConstructor
325+
@AllArgsConstructor
326+
public static class PanelResponseValues {
327+
@JsonProperty("respondent_id")
328+
private String respondentId;
329+
330+
private String gender;
331+
332+
private String age;
333+
334+
private String residence;
335+
336+
@JsonProperty("personal_income")
337+
private String personalIncome;
338+
}
339+
}
340+
248341
}

src/main/java/DiffLens/back_end/domain/library/service/LibraryService.java

Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -22,11 +22,20 @@
2222
import DiffLens.back_end.domain.search.repository.FilterRepository;
2323
import DiffLens.back_end.domain.search.repository.SearchFilterRepository;
2424
import DiffLens.back_end.domain.search.repository.SearchHistoryRepository;
25+
import DiffLens.back_end.domain.panel.repository.projection.PanelWithRawDataDTO;
26+
import DiffLens.back_end.domain.search.service.interfaces.SearchPanelService;
27+
import DiffLens.back_end.global.dto.ResponsePageDTO;
2528
import DiffLens.back_end.global.fastapi.FastApiService;
29+
import DiffLens.back_end.global.fastapi.dto.request.FastLibraryChartRequestDTO;
30+
import DiffLens.back_end.global.fastapi.dto.response.FastChartResponseDTO;
31+
import DiffLens.back_end.global.fastapi.dto.response.FastLibraryChartResponseDTO;
2632
import DiffLens.back_end.global.fastapi.dto.response.FastLibraryCompareResponseDTO;
2733
import DiffLens.back_end.global.responses.code.status.error.ErrorStatus;
2834
import DiffLens.back_end.global.responses.exception.handler.ErrorHandler;
2935
import lombok.RequiredArgsConstructor;
36+
import org.springframework.data.domain.Page;
37+
import org.springframework.data.domain.PageRequest;
38+
import org.springframework.data.domain.Pageable;
3039
import org.springframework.stereotype.Service;
3140
import org.springframework.transaction.annotation.Transactional;
3241

@@ -46,6 +55,7 @@ public class LibraryService {
4655
private final FastApiService fastApiService;
4756
private final SearchFilterRepository searchFilterRepository;
4857
private final FilterRepository filterRepository;
58+
private final SearchPanelService searchPanelService;
4959

5060
@Transactional
5161
public LibraryCreateResult createLibrary(LibraryRequestDto.Create request, Member member) {
@@ -647,6 +657,169 @@ private void createLibraryPanels(Library library, List<String> panelIds) {
647657
libraryPanelRepository.saveAll(libraryPanels);
648658
}
649659

660+
/**
661+
* 라이브러리 대시보드 조회 (차트 포함)
662+
*/
663+
@Transactional(readOnly = true)
664+
public LibraryResponseDTO.LibraryDashboard getLibraryDashboard(Long libraryId, Member member) {
665+
// 1. 라이브러리 조회 및 권한 검증
666+
Library library = libraryRepository.findById(libraryId)
667+
.orElseThrow(() -> new ErrorHandler(ErrorStatus.BAD_REQUEST));
668+
669+
if (!library.getMember().getId().equals(member.getId())) {
670+
throw new ErrorHandler(ErrorStatus.FORBIDDEN);
671+
}
672+
673+
// 2. 패널 ID 배열 조회
674+
List<String> panelIds = library.getPanelIds();
675+
if (panelIds == null || panelIds.isEmpty()) {
676+
throw new ErrorHandler(ErrorStatus.BAD_REQUEST);
677+
}
678+
679+
// 3. 서브서버 API 호출
680+
FastLibraryChartRequestDTO request = FastLibraryChartRequestDTO.builder()
681+
.panelIds(panelIds)
682+
.libraryName(library.getLibraryName())
683+
.build();
684+
685+
FastLibraryChartResponseDTO.LibraryChartResponse chartResponse = fastApiService
686+
.getChartsFromLibrary(request);
687+
688+
// 4. 차트 데이터 변환
689+
LibraryResponseDTO.LibraryDashboard.ChartData mainChart = convertToChartData(
690+
chartResponse.getMainChart());
691+
List<LibraryResponseDTO.LibraryDashboard.ChartData> subCharts = chartResponse.getSubCharts()
692+
.stream()
693+
.map(this::convertToChartData)
694+
.toList();
695+
696+
// 5. 응답 구성
697+
return LibraryResponseDTO.LibraryDashboard.builder()
698+
.libraryId(library.getId())
699+
.libraryName(library.getLibraryName())
700+
.panelCount(panelIds.size())
701+
.mainChart(mainChart)
702+
.subCharts(subCharts)
703+
.build();
704+
}
705+
706+
/**
707+
* 라이브러리 패널 목록 조회 (페이징, 일치율 없음)
708+
*/
709+
@Transactional(readOnly = true)
710+
public LibraryResponseDTO.LibraryPanels getLibraryPanels(Long libraryId, Integer pageNum, Integer size,
711+
Member member) {
712+
// 1. 페이지 번호 예외처리
713+
if (pageNum < 1) {
714+
throw new ErrorHandler(ErrorStatus.PAGE_NO_INVALID);
715+
}
716+
717+
// 2. 라이브러리 조회 및 권한 검증
718+
Library library = libraryRepository.findById(libraryId)
719+
.orElseThrow(() -> new ErrorHandler(ErrorStatus.BAD_REQUEST));
720+
721+
if (!library.getMember().getId().equals(member.getId())) {
722+
throw new ErrorHandler(ErrorStatus.FORBIDDEN);
723+
}
724+
725+
// 3. 패널 ID 배열 조회
726+
List<String> panelIds = library.getPanelIds();
727+
if (panelIds == null || panelIds.isEmpty()) {
728+
return LibraryResponseDTO.LibraryPanels.builder()
729+
.keys(List.of("respondent_id", "gender", "age", "residence",
730+
"personal_income"))
731+
.values(List.of())
732+
.pageInfo(ResponsePageDTO.OffsetLimitPageInfo.builder()
733+
.offset(0)
734+
.currentPage(1)
735+
.currentPageCount(0)
736+
.totalPageCount(0)
737+
.limit(size)
738+
.totalCount(0L)
739+
.hasNext(false)
740+
.hasPrevious(false)
741+
.build())
742+
.build();
743+
}
744+
745+
// 4. 페이징을 위한 Pageable 객체 생성
746+
Pageable pageable = PageRequest.of(pageNum - 1, size);
747+
748+
// 5. PanelId 목록을 이용해서 Panel 조회
749+
Page<PanelWithRawDataDTO> panelDtoList = searchPanelService.getPanelDtoList(panelIds, pageable);
750+
751+
// 6. 페이지 범위 초과 검사
752+
if (pageNum > panelDtoList.getTotalPages() && panelDtoList.getTotalPages() > 0) {
753+
throw new ErrorHandler(ErrorStatus.PAGE_NO_EXCEED);
754+
}
755+
756+
// 7. Panel 목록을 응답 형식으로 변환 (일치율 없음)
757+
List<LibraryResponseDTO.LibraryPanels.PanelResponseValues> values = panelDtoList.stream()
758+
.map(panel -> LibraryResponseDTO.LibraryPanels.PanelResponseValues.builder()
759+
.respondentId(panel.getId())
760+
.gender(panel.getGender() != null ? panel.getGender().getDisplayValue()
761+
: null)
762+
.age(panel.getAge() != null ? panel.getAge().toString() : null)
763+
.residence(panel.getResidence())
764+
.personalIncome(panel.getPersonalIncome())
765+
.build())
766+
.toList();
767+
768+
// 8. 페이징 정보 생성
769+
ResponsePageDTO.OffsetLimitPageInfo pageInfo = ResponsePageDTO.OffsetLimitPageInfo
770+
.from(panelDtoList);
771+
772+
return LibraryResponseDTO.LibraryPanels.builder()
773+
.keys(List.of("respondent_id", "gender", "age", "residence", "personal_income"))
774+
.values(values)
775+
.pageInfo(pageInfo)
776+
.build();
777+
}
778+
779+
/**
780+
* FastAPI ChartData를 LibraryDashboard ChartData로 변환
781+
*/
782+
private LibraryResponseDTO.LibraryDashboard.ChartData convertToChartData(
783+
FastChartResponseDTO.ChartData fastChartData) {
784+
if (fastChartData == null) {
785+
return null;
786+
}
787+
788+
List<LibraryResponseDTO.LibraryDashboard.ChartDataPoint> dataPoints = fastChartData.getData()
789+
.stream()
790+
.map(this::convertToChartDataPoint)
791+
.toList();
792+
793+
return LibraryResponseDTO.LibraryDashboard.ChartData.builder()
794+
.chartType(fastChartData.getChartType())
795+
.metric(fastChartData.getMetric())
796+
.title(fastChartData.getTitle())
797+
.reasoning(fastChartData.getReasoning())
798+
.data(dataPoints)
799+
.build();
800+
}
801+
802+
/**
803+
* FastAPI ChartDataPoint를 LibraryDashboard ChartDataPoint로 변환
804+
*/
805+
private LibraryResponseDTO.LibraryDashboard.ChartDataPoint convertToChartDataPoint(
806+
FastChartResponseDTO.ChartDataPoint fastDataPoint) {
807+
if (fastDataPoint == null) {
808+
return null;
809+
}
810+
811+
return LibraryResponseDTO.LibraryDashboard.ChartDataPoint.builder()
812+
.category(fastDataPoint.getCategory())
813+
.value(fastDataPoint.getValue())
814+
.male(fastDataPoint.getMale())
815+
.maleMax(fastDataPoint.getMaleMax())
816+
.female(fastDataPoint.getFemale())
817+
.femaleMax(fastDataPoint.getFemaleMax())
818+
.id(fastDataPoint.getId())
819+
.name(fastDataPoint.getName())
820+
.build();
821+
}
822+
650823
// 라이브러리 생성 결과를 담는 내부 클래스
651824
@lombok.Getter
652825
@lombok.AllArgsConstructor

src/main/java/DiffLens/back_end/global/fastapi/FastApiRequestType.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
package DiffLens.back_end.global.fastapi;
22

3+
import DiffLens.back_end.global.fastapi.dto.request.FastLibraryChartRequestDTO;
34
import DiffLens.back_end.global.fastapi.dto.request.*;
5+
import DiffLens.back_end.global.fastapi.dto.response.FastLibraryChartResponseDTO;
46
import DiffLens.back_end.global.fastapi.dto.response.*;
57
import lombok.AllArgsConstructor;
68
import lombok.Getter;
@@ -30,6 +32,8 @@ public enum FastApiRequestType {
3032
// 차트
3133
CHART_RECOMMENDATIONS("/api/chart/search-result/{searchId}/recommendations", Void.class,
3234
FastChartResponseDTO.ChartRecommendationsResponse.class),
35+
CHART_FROM_LIBRARY("/api/chart/from-library", FastLibraryChartRequestDTO.class,
36+
FastLibraryChartResponseDTO.LibraryChartResponse.class),
3337
// REFINE_SEARCH("/search/refine",
3438
// FastNaturalSearchResponseDTO.SearchResult.class),
3539
;

0 commit comments

Comments
 (0)