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

[#96] 공연 추천 구현 #100

Merged
merged 5 commits into from
Aug 21, 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
2 changes: 1 addition & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -69,7 +69,7 @@ dependencies {
implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'

// JSONObject
// implementation 'org.json:json:20230227'
implementation 'org.json:json:20230227'

// xml
implementation 'javax.xml.bind:jaxb-api:2.3.1'
Expand Down
15 changes: 15 additions & 0 deletions src/main/java/kopis/k_backend/global/entity/Coordinates.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package kopis.k_backend.global.entity;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class Coordinates {
private String x;
private String y;
}
75 changes: 75 additions & 0 deletions src/main/java/kopis/k_backend/global/service/AddressService.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package kopis.k_backend.global.service;

import kopis.k_backend.global.entity.Coordinates;
import lombok.RequiredArgsConstructor;
import org.json.JSONArray;
import org.json.JSONObject;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.util.UriComponents;
import org.springframework.web.util.UriComponentsBuilder;

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class AddressService {

private final String uri = "https://dapi.kakao.com/v2/local/search/address.json";

@Value("${kakao.local.key}")
private String kakaoLocalKey;

public Coordinates getCoordinate(String address){

// 요청에 담아보낼 API key와 address 생성
String apiKey = "KakaoAK " + kakaoLocalKey;

// 요청 헤더에 만들기, Authorization 헤더 설정
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.set("Authorization", apiKey);
HttpEntity<String> entity = new HttpEntity<>(httpHeaders);

// HTTP 요청에 포함할 쿼리 작성
UriComponents uriComponents = UriComponentsBuilder
.fromHttpUrl(uri)
.queryParam("query",address)
.build();

// API 요청 보내기 위해 spring에서 제공하는 RestTemplate 사용
RestTemplate restTemplate = new RestTemplate();
// exchange(uri를 String 형태로, Http 메서드, HttpEntity, 반환받을 변수 형식)
ResponseEntity<String> response = restTemplate.exchange(uriComponents.toString(), HttpMethod.GET, entity, String.class);

// API Response로부터 body 뽑아내기
String body = response.getBody();
JSONObject json = new JSONObject(body);

// body에서 좌표 뽑아내기
JSONArray documents = json.getJSONArray("documents");
String x = documents.getJSONObject(0).getString("x");
String y = documents.getJSONObject(0).getString("y");

return new Coordinates(x, y);
}

private static final double EARTH_RADIUS = 6371; // 지구 반경 (단위: km)

public static double calculateDistance(double lat1, double lon1, double lat2, double lon2) {
double dLat = Math.toRadians(lat2 - lat1);
double dLon = Math.toRadians(lon2 - lon1);

double a = Math.sin(dLat / 2) * Math.sin(dLat / 2) +
Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) *
Math.sin(dLon / 2) * Math.sin(dLon / 2);

double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a));
return EARTH_RADIUS * c; // 단위: km
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,24 +9,25 @@
import kopis.k_backend.performance.converter.PerformanceConverter;
import kopis.k_backend.performance.domain.*;
import kopis.k_backend.performance.dto.ActorResponseDto.PerformanceDetailActorListResDto;
import kopis.k_backend.performance.dto.PerformanceResponseDto;
import kopis.k_backend.performance.dto.PerformanceResponseDto.PerformanceDetailResDto;
import kopis.k_backend.performance.dto.PerformanceResponseDto.RankPerformanceListDto;
import kopis.k_backend.performance.service.PerformanceRankingService;
import kopis.k_backend.performance.service.PerformanceRecommendService;
import kopis.k_backend.performance.service.PerformanceService;
import kopis.k_backend.user.domain.User;
import kopis.k_backend.user.jwt.CustomUserDetails;
import kopis.k_backend.user.service.UserService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Tag(name = "공연", description = "공연 관련 api 입니다.")
@Slf4j
Expand All @@ -37,6 +38,7 @@ public class PerformanceController {
private final PerformanceRankingService performanceRankingService;
private final UserService userService;
private final PerformanceService performanceService;
private final PerformanceRecommendService performanceRecommendService;

@Operation(summary = "뮤지컬 인기 순위", description = "뮤지컬 인기 순위를 반환하는 api입니다. ")
@ApiResponses({
Expand Down Expand Up @@ -113,4 +115,52 @@ public ApiResponse<PerformanceDetailResDto> getPerformanceDetail(
return ApiResponse.onSuccess(SuccessCode.PERFORMANCE_DETAIL_SUCCESS, PerformanceConverter.performanceDetailResDto(performance, performanceDetailActorListResDto));
}

@Operation(summary = "공연 추천", description = "추천하는 공연 리스트를 반환하는 메서드입니다.")
@ApiResponses({
@io.swagger.v3.oas.annotations.responses.ApiResponse(responseCode = "PERFORMANCE_RECOMMEND_2005", description = "추천하는 공연 리스트를 반환했습니다.")
})
@GetMapping("/recommend")
public ApiResponse<PerformanceResponseDto.StandardRecommendPerfListDto> getRecommendPerformance(
@AuthenticationPrincipal CustomUserDetails customUserDetails,
@RequestParam(name = "type") Integer type,
@RequestParam(name = "startYear") Integer startYear,
@RequestParam(name = "startMonth") Integer startMonth,
@RequestParam(name = "startDate") Integer startDate,
@RequestParam(name = "endYear") Integer endYear,
@RequestParam(name = "endMonth") Integer endMonth,
@RequestParam(name = "endDate") Integer endDate,
@RequestParam(name = "location") String location,
@RequestParam(name = "minPrice") Integer minPrice,
@RequestParam(name = "maxPrice") Integer maxPrice
) {
User user = userService.findByUserName(customUserDetails.getUsername());
// 기본 요건 충족되는 공연 추출
List<Performance> performances = performanceRecommendService.getRecommendPerformance(type, startYear, startMonth, startDate, endYear, endMonth, endDate, location, minPrice, maxPrice);

// 1. 별점 높고 리뷰 많은 공연 10개
List<Performance> topRatedPerformances = performanceRecommendService.getTopRatedPerformances(performances);
// 2. 내가 찜한 배우의 공연 전체 (공연 중 or 공연 예정)
List<Performance> favoriteActorPerformances = performanceRecommendService.getPerformancesByFavoriteActors(user);
// 3. 시작이 임박한 공연 10개
List<Performance> upcomingPerformances = performanceRecommendService.getUpcomingPerformances(performances);
// 4. 마감이 임박한 공연 10개
List<Performance> upcomingEndPerformances = performanceRecommendService.getPerformancesWithUpcomingEndDate(performances);
// 5. 사용자 주소와 거리가 가까운 공연 10개 추천
List<Performance> nearestPerformances = performanceRecommendService.getPerformancesSortedByDistance(user, performances);


// 각 추천 리스트를 StandardRecommendPerfDto로 변환하여 추가
Map<String, List<Performance>> performancesByStandardMap = new HashMap<>();
performancesByStandardMap.put("리뷰 많고 별점 높은 공연", topRatedPerformances);
performancesByStandardMap.put("좋아하는 배우의 공연", favoriteActorPerformances);
performancesByStandardMap.put("시작이 임박한 공연", upcomingPerformances);
performancesByStandardMap.put("마감이 임박한 공연", upcomingEndPerformances);
performancesByStandardMap.put("내 주소와 가까운 공연", nearestPerformances);

// 전체 리스트를 StandardRecommendPerfListDto로 변환
PerformanceResponseDto.StandardRecommendPerfListDto responseDto = PerformanceConverter.standardRecommendListResDto(performancesByStandardMap);

return ApiResponse.onSuccess(SuccessCode.PERFORMANCE_DETAIL_SUCCESS, responseDto);
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,17 @@

import kopis.k_backend.performance.domain.*;
import kopis.k_backend.performance.dto.ActorResponseDto.PerformanceDetailActorListResDto;
import kopis.k_backend.performance.dto.PerformanceResponseDto;
import kopis.k_backend.performance.dto.PerformanceResponseDto.PerformanceDetailResDto;
import kopis.k_backend.performance.dto.PerformanceResponseDto.SimpleRankPerformanceDto;
import kopis.k_backend.performance.dto.PerformanceResponseDto.RankPerformanceListDto;
import kopis.k_backend.performance.domain.Performance;
import kopis.k_backend.performance.dto.PerformanceResponseDto.HomeSearchPerformanceResDto;
import kopis.k_backend.performance.dto.PerformanceResponseDto.HomeSearchPerformanceListResDto;

import lombok.NoArgsConstructor;
import org.springframework.data.domain.Page;

import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@NoArgsConstructor
Expand Down Expand Up @@ -153,4 +153,39 @@ public static PerformanceDetailResDto performanceDetailResDto(Performance perfor
.build();
}

public static PerformanceResponseDto.SimpleRecommendPerfDto simpleRecommendResDto(Performance performance){
return PerformanceResponseDto.SimpleRecommendPerfDto.builder()
.id(performance.getId())
.title(performance.getTitle())
.poster(performance.getPoster())
.ratingAverage(performance.getRatingAverage())

.price(performance.getPrice())
.startDate(performance.getStartDate())
.endDate(performance.getEndDate())
.build();
}

public static PerformanceResponseDto.StandardRecommendPerfDto standardRecommendResDto(String standard, List<Performance> performanceList){
List<PerformanceResponseDto.SimpleRecommendPerfDto> recommendResDtos = performanceList.stream()
.map(PerformanceConverter::simpleRecommendResDto)
.toList();

return PerformanceResponseDto.StandardRecommendPerfDto.builder()
.standard(standard)
.performancesByStandard(recommendResDtos)
.build();

}

public static PerformanceResponseDto.StandardRecommendPerfListDto standardRecommendListResDto(Map<String, List<Performance>> performancesByStandardMap) {
List<PerformanceResponseDto.StandardRecommendPerfDto> recommendResDtos = performancesByStandardMap.entrySet().stream()
.map(entry -> standardRecommendResDto(entry.getKey(), entry.getValue()))
.toList();

return PerformanceResponseDto.StandardRecommendPerfListDto.builder()
.performancesByStandardList(recommendResDtos)
.build();
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -150,4 +150,61 @@ public static class PerformanceDetailResDto {

}

@Schema(description = "SimpleRecommendPerfDto")
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class SimpleRecommendPerfDto {

@Schema(description = "공연 id - 이걸로 아래 리뷰 가져오면 돼요")
private Long id;

@Schema(description = "공연 제목")
private String title;

@Schema(description = "공연 포스터")
private String poster;

@Schema(description = "평점")
private Double ratingAverage;

@Schema(description = "시작날짜")
private String startDate;

@Schema(description = "종료날짜")
private String endDate;

@Schema(description = "가격")
private String price;

}

@Schema(description = "StandardRecommendPerfDto")
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class StandardRecommendPerfDto {

@Schema(description = "추천 기준")
private String standard;

@Schema(description = "공연 리스트")
private List<SimpleRecommendPerfDto> performancesByStandard;

}

@Schema(description = "StandardRecommendPerfListDto")
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public static class StandardRecommendPerfListDto {

@Schema(description = "공연 리스트")
private List<StandardRecommendPerfDto> performancesByStandardList;

}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kopis.k_backend.performance.repository;

import kopis.k_backend.performance.domain.Performance;
import kopis.k_backend.performance.domain.PerformanceType;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -22,4 +23,31 @@ public interface PerformanceRepository extends JpaRepository<Performance, Long>

@Query("SELECT p FROM Performance p WHERE p.reviewCount > :minReviews AND p.state != '공연완료' ORDER BY p.ratingAverage DESC, p.reviewCount ASC")
List<Performance> findTop10ByMinReviewsOrderByRatingAndReviewCount(@Param("minReviews") Long minReviews);

// 공연의 시작날짜가 주어진 endDate보다 작거나 같음 & 공연의 종료 날짜가 주어진 startDate보다 크거나 같음
// 주어진 가격의 최솟값이 공연의 최댓값보다 작거가 같음 & 주어진 가격의 최댓값이 공연의 최솟값보다 크거나 같음
@Query(value = "SELECT p.* FROM performance p " +
"JOIN hall h ON p.hall_id = h.id " +
"WHERE p.performance_type = :performanceType " +
"AND p.start_date <= :endDate " +
"AND p.end_date >= :startDate " +
"AND h.sidonm = :location " +
"AND CAST(p.lowest_price AS UNSIGNED) <= :maxPrice " +
"AND CAST(p.highest_price AS UNSIGNED) >= :minPrice", nativeQuery = true)
List<Performance> findPerformancesByCriteria(
@Param("performanceType") PerformanceType performanceType,
@Param("endDate") String endDate,
@Param("startDate") String startDate,
@Param("location") String location,
@Param("minPrice") Integer minPrice,
@Param("maxPrice") Integer maxPrice
);

// 찜한 배우의 공연 반환
@Query("SELECT p FROM Performance p " +
"JOIN p.performanceActors pa " +
"JOIN pa.actor a " +
"JOIN a.favoriteActors fa " +
"WHERE fa.user.id = :userId AND (p.state = '공연중' OR p.state = '공연예정')")
List<Performance> findPerformancesByFavoriteActorsAndState(@Param("userId") Long userId);
}
Loading
Loading