Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[#92] 전체, 연간, 월간 행복 종합 리포트 API 구축 #93

Merged
merged 7 commits into from
May 26, 2024
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 @@ -13,4 +13,5 @@ public interface RecordRepository extends JpaRepository<Record, Long> {
Page<Record> findByUserOrderByRecordIdDesc(User user, Pageable pageRequest);
Page<Record> findByRecordIdLessThanAndUserOrderByRecordIdDesc(Long recordId, User user, Pageable pageRequest);
List<Record> findAllByCreatedAtBetweenAndUser(LocalDateTime startOfMonth, LocalDateTime endOfMonth, User user);
List<Record> findAllByUser(User user);
}
Original file line number Diff line number Diff line change
@@ -1,15 +1,50 @@
package com.hobak.happinessql.domain.report.api;


import com.hobak.happinessql.domain.report.application.ReportSummaryService;
import com.hobak.happinessql.domain.report.dto.ReportSummaryResponseDto;
import com.hobak.happinessql.domain.user.application.UserFindService;
import com.hobak.happinessql.domain.user.domain.User;
import com.hobak.happinessql.global.response.DataResponseDto;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Tag(name="Report", description = "행복 분석 리포트 관련 REST API에 대한 명세를 제공합니다.")
@RestController
@RequiredArgsConstructor
@RequestMapping("api/my-report")
@RequestMapping("api/report")
public class ReportController {

private final UserFindService userFindService;
private final ReportSummaryService reportSummaryService;
@Operation(summary = "[전체] 행복 종합 리포트", description = "언제, 어디에서, 무엇을 할 때 행복했는지에 대한 종합적인 리포트를 제공합니다.")
@GetMapping("/all/summary")
public DataResponseDto<ReportSummaryResponseDto> getAllSummary(@AuthenticationPrincipal UserDetails userDetails) {
User user = userFindService.findByUserDetails(userDetails);
ReportSummaryResponseDto responseDto = reportSummaryService.getAllSummary(user);
return DataResponseDto.of(responseDto, "행복 종합 리포트(전체)를 성공적으로 조회했습니다.");
}

@Operation(summary = "[연간] 행복 종합 리포트", description = "이번 해 언제, 어디에서, 무엇을 할 때 행복했는지에 대한 종합적인 리포트를 제공합니다.")
@GetMapping("/year/summary")
public DataResponseDto<ReportSummaryResponseDto> getAnnualSummary(@AuthenticationPrincipal UserDetails userDetails) {
User user = userFindService.findByUserDetails(userDetails);
ReportSummaryResponseDto responseDto = reportSummaryService.getAnnualSummary(user);
return DataResponseDto.of(responseDto, "행복 종합 리포트(연간)를 성공적으로 조회했습니다.");
}

@Operation(summary = "[월간] 행복 종합 리포트", description = "이번 달 언제, 어디에서, 무엇을 할 때 행복했는지에 대한 종합적인 리포트를 제공합니다.")
@GetMapping("/month/summary")
public DataResponseDto<ReportSummaryResponseDto> getMonthlySummary(@AuthenticationPrincipal UserDetails userDetails) {
User user = userFindService.findByUserDetails(userDetails);
ReportSummaryResponseDto responseDto = reportSummaryService.getMonthlySummary(user);
return DataResponseDto.of(responseDto, "행복 종합 리포트(월간)를 성공적으로 조회했습니다.");
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package com.hobak.happinessql.domain.report.application;

import com.hobak.happinessql.domain.record.domain.Record;

import java.util.*;
import java.util.stream.Collectors;

public class ActivityHappinessAnalyzer {

public static String getHappiestActivity(List<Record> records) {

if (records == null || records.isEmpty()) {
return null;
}

// Activity를 기준으로 Record 그룹화
Map<String, List<Record>> activityRecordsMap = records.stream()
.filter(record -> record.getActivity() != null)
.collect(Collectors.groupingBy(record -> record.getActivity().getName()));

// 각 Activity별 평균 행복도와 빈도 계산
Map<String, Double> activityAverageHappiness = new HashMap<>();
Map<String, Integer> activityFrequency = new HashMap<>();

activityRecordsMap.forEach((activity, recordList) -> {
activityAverageHappiness.put(activity, recordList.stream()
.mapToInt(Record::getHappiness)
.average()
.orElse(Double.NaN));
activityFrequency.put(activity, recordList.size());
});

// 평균 행복도가 가장 높은 Activity 찾기
double maxHappiness = Collections.max(activityAverageHappiness.values());



// 평균 행복도가 가장 높은 Activity들 중 빈도가 가장 높은 Activity 찾기
Optional<String> happiestActivity = activityAverageHappiness.entrySet().stream()
.filter(entry -> entry.getValue() == maxHappiness)
.max(Comparator.comparing(entry -> activityFrequency.get(entry.getKey())))
.map(Map.Entry::getKey);

System.out.println("평균 행복도가 높은 Activity : " + happiestActivity + "행복도 : " + maxHappiness);
// 평균 행복도와 빈도가 같은 Activity가 여러 개인 경우, 랜덤으로 선택
List<String> happiestActivities = activityAverageHappiness.entrySet().stream()
.filter(entry -> entry.getValue() == maxHappiness)
.map(Map.Entry::getKey)
.collect(Collectors.toList());

if (happiestActivities.size() > 1) {
Collections.shuffle(happiestActivities);
return happiestActivities.get(0);
} else {
return happiestActivity.orElse(null);
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
package com.hobak.happinessql.domain.report.application;

import com.hobak.happinessql.domain.record.domain.Record;

import java.util.*;
import java.util.stream.Collectors;

public class LocationHappinessAnalyzer {

public static String getHappiestLocation(List<Record> records) {

if (records == null || records.isEmpty()) {
return null;
}

// 도시와 구를 기준으로 Record 그룹화
Map<String, List<Record>> locationRecordsMap = records.stream()
.filter(record -> record.getLocation() != null)
.collect(Collectors.groupingBy(record ->
record.getLocation().getCity() + " " + record.getLocation().getDistrict()));

// 각 위치별 평균 행복도와 빈도 계산
Map<String, Double> locationAverageHappiness = new HashMap<>();
Map<String, Integer> locationFrequency = new HashMap<>();

locationRecordsMap.forEach((location, recordList) -> {
locationAverageHappiness.put(location, recordList.stream()
.mapToInt(Record::getHappiness)
.average()
.orElse(Double.NaN));
locationFrequency.put(location, recordList.size());
});

// 평균 행복도가 가장 높은 위치 찾기
double maxHappiness = Collections.max(locationAverageHappiness.values());

// 평균 행복도가 가장 높은 위치들 중 빈도가 가장 높은 위치를 찾기
Optional<String> happiestLocation = locationAverageHappiness.entrySet().stream()
.filter(entry -> entry.getValue() == maxHappiness)
.max(Comparator.comparing(entry -> locationFrequency.get(entry.getKey())))
.map(Map.Entry::getKey);

// 평균 행복도와 빈도가 같다면, 랜덤으로 선택
List<String> happiestLocations = locationAverageHappiness.entrySet().stream()
.filter(entry -> entry.getValue() == maxHappiness)
.map(Map.Entry::getKey)
.collect(Collectors.toList());

if (happiestLocations.size() > 1) {
Collections.shuffle(happiestLocations);
return happiestLocations.get(0);
} else {
return happiestLocation.orElse(null);
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
package com.hobak.happinessql.domain.report.application;

import com.hobak.happinessql.domain.record.domain.Record;
import com.hobak.happinessql.domain.record.repository.RecordRepository;
import com.hobak.happinessql.domain.report.converter.ReportConverter;
import com.hobak.happinessql.domain.report.domain.TimePeriod;
import com.hobak.happinessql.domain.report.dto.ReportSummaryResponseDto;
import com.hobak.happinessql.domain.user.domain.User;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;

import java.util.List;

@Service
@RequiredArgsConstructor
public class ReportSummaryService {

private final RecordRepository recordRepository;

public ReportSummaryResponseDto getAllSummary(User user) {
List<Record> records = recordRepository.findAllByUser(user);
return generateReportSummary(records);
}

public ReportSummaryResponseDto getAnnualSummary(User user) {
int currentYear = LocalDate.now().getYear();
LocalDateTime startOfYear = LocalDateTime.of(currentYear, 1, 1, 0, 0);
LocalDateTime endOfYear = LocalDateTime.of(currentYear, 12, 31, 23, 59, 59);

List<Record> annualRecords = recordRepository.findAllByCreatedAtBetweenAndUser(startOfYear, endOfYear, user);
return generateReportSummary(annualRecords);
}

public ReportSummaryResponseDto getMonthlySummary(User user) {
LocalDate today = LocalDate.now();
LocalDateTime startOfMonth = today.withDayOfMonth(1).atStartOfDay();
LocalDateTime endOfMonth = today.withDayOfMonth(today.lengthOfMonth()).atTime(23, 59, 59);

List<Record> monthlyRecords = recordRepository.findAllByCreatedAtBetweenAndUser(startOfMonth, endOfMonth, user);
return generateReportSummary(monthlyRecords);
}

private ReportSummaryResponseDto generateReportSummary(List<Record> records) {
String location = LocationHappinessAnalyzer.getHappiestLocation(records);
TimePeriod timePeriod = TimePeriodHappinessAnalyzer.getHappiestTimePeriod(records);
String activity = ActivityHappinessAnalyzer.getHappiestActivity(records);

return ReportConverter.toReportSummaryResponseDto(timePeriod, location, activity);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.hobak.happinessql.domain.report.application;

import com.hobak.happinessql.domain.record.domain.Record;
import com.hobak.happinessql.domain.report.domain.TimePeriod;

import java.util.*;
import java.util.stream.Collectors;

public class TimePeriodHappinessAnalyzer {

public static TimePeriod getHappiestTimePeriod(List<Record> records) {

if (records == null || records.isEmpty()) {
return null;
}

Map<TimePeriod, List<Integer>> timePeriodHappinessMap = new HashMap<>();

// 시간대별 행복도를 매핑
for (Record record : records) {
TimePeriod timePeriod = TimePeriod.of(record.getCreatedAt().getHour());
Integer happiness = record.getHappiness();

timePeriodHappinessMap.computeIfAbsent(timePeriod, k -> new ArrayList<>()).add(happiness);
}

// 평균 행복도 계산
Map<TimePeriod, Double> averageHappinessMap = timePeriodHappinessMap.entrySet().stream()
.collect(Collectors.toMap(Map.Entry::getKey,
entry -> entry.getValue().stream().mapToInt(Integer::intValue).average().orElse(0)));

// 최대 평균 행복도를 가진 시간대 찾기
double maxAverageHappiness = averageHappinessMap.values().stream()
.max(Double::compare)
.orElse(Double.MIN_VALUE);

// 최대 평균 행복도를 가진 시간대 후보들 필터링
List<TimePeriod> candidates = averageHappinessMap.entrySet().stream()
.filter(entry -> entry.getValue() == maxAverageHappiness)
.map(Map.Entry::getKey)
.toList();

if (candidates.isEmpty()) {
return null;
}

// 후보 중 빈도수가 가장 높은 시간대 찾기, 동일 빈도시 랜덤 선택
return candidates.stream()
.max(Comparator.comparingInt(tp -> timePeriodHappinessMap.get(tp).size()))
.orElseGet(() -> candidates.get(new Random().nextInt(candidates.size())));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.hobak.happinessql.domain.report.converter;

import com.hobak.happinessql.domain.report.domain.TimePeriod;
import com.hobak.happinessql.domain.report.dto.ReportSummaryResponseDto;

public class ReportConverter {
public static ReportSummaryResponseDto toReportSummaryResponseDto(TimePeriod timePeriod, String location, String activity) {
return ReportSummaryResponseDto.builder()
.activity(activity)
.location(location)
.timePeriod(timePeriod)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.hobak.happinessql.domain.report.domain;

import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.AllArgsConstructor;

@AllArgsConstructor
public enum TimePeriod {
DAWN("새벽"),
MORNING("아침"),
AFTERNOON("낮"),
EVENING("저녁"),
NIGHT("밤");

private final String viewName;

public static TimePeriod of(int hour) {
if (hour >= 0 && hour < 5) {
return DAWN;
} else if (hour >= 5 && hour < 9) {
return MORNING;
} else if (hour >= 9 && hour < 17) {
return AFTERNOON;
} else if (hour >= 17 && hour < 21) {
return EVENING;
} else {
return NIGHT;
}
}

@JsonCreator
public static TimePeriod from(String value) {
for (TimePeriod status : TimePeriod.values()) {
if (status.getViewName().equals(value)) {
return status;
}
}
return null;
}

@JsonValue
public String getViewName() {
return viewName;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package com.hobak.happinessql.domain.report.dto;

import com.fasterxml.jackson.annotation.JsonProperty;
import com.hobak.happinessql.domain.report.domain.TimePeriod;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;


@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ReportSummaryResponseDto {

@JsonProperty("time_period")
private TimePeriod timePeriod;

private String location;

private String activity;

@Builder
public ReportSummaryResponseDto(TimePeriod timePeriod, String location, String activity) {
this.timePeriod = timePeriod;
this.location = location;
this.activity = activity;
}
}
Loading