Skip to content

Commit

Permalink
feat : 행복 위치 Best 3 API 구축
Browse files Browse the repository at this point in the history
  • Loading branch information
yel-m committed May 26, 2024
1 parent 2a8e89f commit b78a7dd
Show file tree
Hide file tree
Showing 6 changed files with 243 additions and 25 deletions.
Original file line number Diff line number Diff line change
@@ -1,9 +1,11 @@
package com.hobak.happinessql.domain.report.api;


import com.hobak.happinessql.domain.report.application.ReportRankingService;
import com.hobak.happinessql.domain.report.application.ReportActivityRankingService;
import com.hobak.happinessql.domain.report.application.ReportLocationRankingService;
import com.hobak.happinessql.domain.report.application.ReportSummaryService;
import com.hobak.happinessql.domain.report.dto.ActivityHappinessDto;
import com.hobak.happinessql.domain.report.dto.LocationHappinessDto;
import com.hobak.happinessql.domain.report.dto.ReportSummaryResponseDto;
import com.hobak.happinessql.domain.user.application.UserFindService;
import com.hobak.happinessql.domain.user.domain.User;
Expand All @@ -27,7 +29,8 @@ public class ReportController {

private final UserFindService userFindService;
private final ReportSummaryService reportSummaryService;
private final ReportRankingService reportRankingService;
private final ReportActivityRankingService reportActivityRankingService;
private final ReportLocationRankingService reportLocationRankingService;
@Operation(summary = "[전체] 행복 종합 리포트", description = "전체 기간에서 언제, 어디에서, 무엇을 할 때 행복했는지에 대한 종합적인 리포트를 제공합니다.")
@GetMapping("/all/summary")
public DataResponseDto<ReportSummaryResponseDto> getAllSummary(@AuthenticationPrincipal UserDetails userDetails) {
Expand Down Expand Up @@ -56,26 +59,50 @@ public DataResponseDto<ReportSummaryResponseDto> getMonthlySummary(@Authenticati
@GetMapping("/all/top-activities")
public DataResponseDto<List<ActivityHappinessDto>> getTop3AllHappiestActivities(@AuthenticationPrincipal UserDetails userDetails) {
User user = userFindService.findByUserDetails(userDetails);
List<ActivityHappinessDto> responseDto = reportRankingService.getTop3AllHappiestActivities(user);
List<ActivityHappinessDto> responseDto = reportActivityRankingService.getTop3AllHappiestActivities(user);
return DataResponseDto.of(responseDto, "행복도가 높은 Top 3 활동(전체)을 성공적으로 조회했습니다.");
}

@Operation(summary = "[연간] 행복도가 높은 Top 3 활동", description = "이번 해 가장 행복도가 높은 Top 3 활동을 제공합니다.")
@GetMapping("/year/top-activities")
public DataResponseDto<List<ActivityHappinessDto>> getTop3AnnualHappiestActivities(@AuthenticationPrincipal UserDetails userDetails) {
User user = userFindService.findByUserDetails(userDetails);
List<ActivityHappinessDto> responseDto = reportRankingService.getTop3AnnualHappiestActivities(user);
List<ActivityHappinessDto> responseDto = reportActivityRankingService.getTop3AnnualHappiestActivities(user);
return DataResponseDto.of(responseDto, "행복도가 높은 Top 3 활동(연간)을 성공적으로 조회했습니다.");
}

@Operation(summary = "[월간] 행복도가 높은 Top 3 활동", description = "이번 해 가장 행복도가 높은 Top 3 활동을 제공합니다.")
@GetMapping("/month/top-activities")
public DataResponseDto<List<ActivityHappinessDto>> getTop3MonthlyHappiestActivities(@AuthenticationPrincipal UserDetails userDetails) {
User user = userFindService.findByUserDetails(userDetails);
List<ActivityHappinessDto> responseDto = reportRankingService.getTop3MonthlyHappiestActivities(user);
List<ActivityHappinessDto> responseDto = reportActivityRankingService.getTop3MonthlyHappiestActivities(user);
return DataResponseDto.of(responseDto, "행복도가 높은 Top 3 활동(월간)을 성공적으로 조회했습니다.");
}

@Operation(summary = "[전체] 행복도가 높은 Top 3 위치", description = "전체 기록에서 가장 행복도가 높은 Top 3 위치을 제공합니다.")
@GetMapping("/all/top-locations")
public DataResponseDto<List<LocationHappinessDto>> getTop3AllHappiestLocations(@AuthenticationPrincipal UserDetails userDetails) {
User user = userFindService.findByUserDetails(userDetails);
List<LocationHappinessDto> responseDto = reportLocationRankingService.getTop3AllHappiestLocations(user);
return DataResponseDto.of(responseDto, "행복도가 높은 Top 3 위치(전체)를 성공적으로 조회했습니다.");
}

@Operation(summary = "[연간] 행복도가 높은 Top 3 위치", description = "이번 해 가장 행복도가 높은 Top 3 위치를 제공합니다.")
@GetMapping("/year/top-locations")
public DataResponseDto<List<LocationHappinessDto>> getTop3AnnualHappiestLocations(@AuthenticationPrincipal UserDetails userDetails) {
User user = userFindService.findByUserDetails(userDetails);
List<LocationHappinessDto> responseDto = reportLocationRankingService.getTop3AnnualHappiestLocations(user);
return DataResponseDto.of(responseDto, "행복도가 높은 Top 3 위치(연간)를 성공적으로 조회했습니다.");
}

@Operation(summary = "[월간] 행복도가 높은 Top 3 위치", description = "이번 해 가장 행복도가 높은 Top 3 위치를 제공합니다.")
@GetMapping("/month/top-locations")
public DataResponseDto<List<LocationHappinessDto>> getTop3MonthlyHappiestLocations(@AuthenticationPrincipal UserDetails userDetails) {
User user = userFindService.findByUserDetails(userDetails);
List<LocationHappinessDto> responseDto = reportLocationRankingService.getTop3MonthlyHappiestLocations(user);
return DataResponseDto.of(responseDto, "행복도가 높은 Top 3 위치(월간)를 성공적으로 조회했습니다.");
}



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

import com.hobak.happinessql.domain.record.domain.Record;
import com.hobak.happinessql.domain.report.converter.ReportConverter;
import com.hobak.happinessql.domain.report.dto.LocationHappinessDto;

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()
Map<String, List<Record>> locationRecordsMap = groupRecordsByLocation(records);

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

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

// 평균 행복도가 가장 높은 위치들 중 빈도가 가장 높은 위치 찾기
Optional<String> happiestLocation = findHappiestLocation(locationAverageHappiness, locationFrequency, maxHappiness);

// 평균 행복도와 빈도가 같다면, 랜덤으로 선택
List<String> happiestLocations = findLocationsWithMaxHappiness(locationAverageHappiness, maxHappiness);

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

public static List<LocationHappinessDto> getLocationRankings(List<Record> records, int topCount) {
List<LocationHappinessDto> locationRankings = new ArrayList<>();
if (records == null || records.isEmpty()) {
// 데이터가 없는 경우에도 빈 LocationHappinessDto 객체를 topCount만큼 추가
for (int i = 0; i < topCount; i++) {
locationRankings.add(ReportConverter.toLocationHappinessDto(i + 1, null));
}
return locationRankings;
}

// 도시와 구를 기준으로 Record 그룹화
Map<String, List<Record>> locationRecordsMap = groupRecordsByLocation(records);

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

// 평균 행복도와 빈도를 기준으로 위치들을 정렬, 동일한 행복도와 빈도일 경우 사전순 정렬
List<String> sortedLocations = sortLocations(locationAverageHappiness, locationFrequency);

// 상위 N개의 위치 선정
for (int i = 0; i < Math.min(topCount, sortedLocations.size()); i++) {
String location = sortedLocations.get(i);
LocationHappinessDto locationDto = ReportConverter.toLocationHappinessDto(i + 1, location);
locationRankings.add(locationDto);
}

// 만약 topCount보다 적게 선정된 경우, 나머지 빈 항목 추가
while (locationRankings.size() < topCount) {
locationRankings.add(ReportConverter.toLocationHappinessDto(locationRankings.size() + 1, null));
}

return locationRankings;
}

private static Map<String, List<Record>> groupRecordsByLocation(List<Record> records) {
return records.stream()
.filter(record -> record.getLocation() != null)
.collect(Collectors.groupingBy(record ->
record.getLocation().getCity() + " " + record.getLocation().getDistrict()));
}

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

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

// 평균 행복도가 가장 높은 위치 찾기
double maxHappiness = Collections.max(locationAverageHappiness.values());
private static Map<String, Integer> calculateLocationFrequency(Map<String, List<Record>> locationRecordsMap) {
Map<String, Integer> locationFrequency = new HashMap<>();
locationRecordsMap.forEach((location, recordList) -> {
locationFrequency.put(location, recordList.size());
});
return locationFrequency;
}

// 평균 행복도가 가장 높은 위치들 중 빈도가 가장 높은 위치를 찾기
Optional<String> happiestLocation = locationAverageHappiness.entrySet().stream()
private static Optional<String> findHappiestLocation(Map<String, Double> locationAverageHappiness,
Map<String, Integer> locationFrequency,
double maxHappiness) {
return 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()
private static List<String> findLocationsWithMaxHappiness(Map<String, Double> locationAverageHappiness,
double maxHappiness) {
return 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);
}
private static List<String> sortLocations(Map<String, Double> locationAverageHappiness,
Map<String, Integer> locationFrequency) {
return locationAverageHappiness.entrySet().stream()
.sorted((entry1, entry2) -> {
double happinessDiff = entry2.getValue() - entry1.getValue();
if (happinessDiff != 0) {
return Double.compare(entry2.getValue(), entry1.getValue());
} else {
int frequencyDiff = locationFrequency.get(entry2.getKey()) - locationFrequency.get(entry1.getKey());
if (frequencyDiff != 0) {
return Integer.compare(locationFrequency.get(entry2.getKey()), locationFrequency.get(entry1.getKey()));
} else {
return entry1.getKey().compareTo(entry2.getKey());
}
}
})
.map(Map.Entry::getKey)
.toList();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
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.dto.ActivityHappinessDto;
import com.hobak.happinessql.domain.user.domain.User;
import lombok.RequiredArgsConstructor;
import java.time.LocalDate;
import java.time.LocalDateTime;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
@RequiredArgsConstructor
public class ReportActivityRankingService {

private final RecordRepository recordRepository;

public List<ActivityHappinessDto> getTop3AllHappiestActivities(User user) {
List<Record> records = recordRepository.findAllByUser(user);
return ActivityHappinessAnalyzer.getActivityRankings(records, 3);
}

public List<ActivityHappinessDto> getTop3AnnualHappiestActivities(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> records = recordRepository.findAllByCreatedAtBetweenAndUser(startOfYear, endOfYear, user);
return ActivityHappinessAnalyzer.getActivityRankings(records, 3);
}

public List<ActivityHappinessDto> getTop3MonthlyHappiestActivities(User user) {
LocalDate today = LocalDate.now();
LocalDate startOfMonth = today.withDayOfMonth(1);
LocalDate endOfMonth = today.withDayOfMonth(today.lengthOfMonth());
LocalDateTime startOfMonthDateTime = startOfMonth.atStartOfDay();
LocalDateTime endOfMonthDateTime = endOfMonth.atTime(23, 59, 59);
List<Record> records = recordRepository.findAllByCreatedAtBetweenAndUser(startOfMonthDateTime, endOfMonthDateTime, user);
return ActivityHappinessAnalyzer.getActivityRankings(records, 3);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
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.dto.LocationHappinessDto;
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 ReportLocationRankingService {
private final RecordRepository recordRepository;

public List<LocationHappinessDto> getTop3AllHappiestLocations(User user) {
List<Record> records = recordRepository.findAllByUser(user);
return LocationHappinessAnalyzer.getLocationRankings(records, 3);
}

public List<LocationHappinessDto> getTop3AnnualHappiestLocations(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> records = recordRepository.findAllByCreatedAtBetweenAndUser(startOfYear, endOfYear, user);
return LocationHappinessAnalyzer.getLocationRankings(records, 3);
}

public List<LocationHappinessDto> getTop3MonthlyHappiestLocations(User user) {
LocalDate today = LocalDate.now();
LocalDate startOfMonth = today.withDayOfMonth(1);
LocalDate endOfMonth = today.withDayOfMonth(today.lengthOfMonth());
LocalDateTime startOfMonthDateTime = startOfMonth.atStartOfDay();
LocalDateTime endOfMonthDateTime = endOfMonth.atTime(23, 59, 59);
List<Record> records = recordRepository.findAllByCreatedAtBetweenAndUser(startOfMonthDateTime, endOfMonthDateTime, user);
return LocationHappinessAnalyzer.getLocationRankings(records, 3);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

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

public class ReportConverter {
Expand All @@ -20,4 +21,11 @@ public static ActivityHappinessDto toActivityHappinessDto(int ranking, String ac
.emoji(emoji)
.build();
}

public static LocationHappinessDto toLocationHappinessDto(int ranking, String location) {
return LocationHappinessDto.builder()
.ranking(ranking)
.location(location)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package com.hobak.happinessql.domain.report.dto;


import lombok.*;

@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class LocationHappinessDto {
private int ranking;
private String location;

@Builder
public LocationHappinessDto(int ranking, String location) {
this.ranking = ranking;
this.location = location;
}
}

0 comments on commit b78a7dd

Please sign in to comment.