Skip to content

Commit

Permalink
merge: pull request #47 from SOPT-all/feat/#46
Browse files Browse the repository at this point in the history
[FEAT#46] 위치 기반 장소 추천 MOCK API 구현
  • Loading branch information
ckkim817 authored Jan 22, 2025
2 parents b9e029f + bd2eccc commit 5b92095
Show file tree
Hide file tree
Showing 7 changed files with 146 additions and 0 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
name = "idx_verified_area_member_id",
columnList = "member_id"
)
// TODO: memberId와 name을 unique로 묶던가 verifiedDate를 위한 테이블을 분리하던가
)
public class VerifiedAreaEntity {

Expand Down
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
package com.acon.server.spot.api.controller;

import com.acon.server.spot.api.request.SpotListRequest;
import com.acon.server.spot.api.response.MenuListResponse;
import com.acon.server.spot.api.response.SearchSpotListResponse;
import com.acon.server.spot.api.response.SearchSuggestionListResponse;
import com.acon.server.spot.api.response.SpotDetailResponse;
import com.acon.server.spot.api.response.SpotListResponse;
import com.acon.server.spot.api.response.VerifiedSpotResponse;
import com.acon.server.spot.application.service.SpotService;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Positive;
Expand All @@ -15,6 +18,8 @@
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
Expand All @@ -26,6 +31,20 @@ public class SpotController {

private final SpotService spotService;

// 위치 및 사용자 온보딩 결과 기반 개인화 장소 리스트 추천 API 컨트롤러 메서드
@PostMapping(
path = "/spots",
consumes = MediaType.APPLICATION_JSON_VALUE,
produces = MediaType.APPLICATION_JSON_VALUE
)
public ResponseEntity<SpotListResponse> getRecommendedSpotList(
@Valid @RequestBody final SpotListRequest request
) {
return ResponseEntity.ok(
spotService.fetchRecommendedSpotList(request)
);
}

@GetMapping(path = "/spot/{spotId}", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<SpotDetailResponse> getSpotDetail(
@Positive(message = "spotId는 양수여야 합니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.acon.server.spot.api.request;

import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import java.util.List;

public record SpotListRequest(
@NotNull(message = "위도는 필수 입력값입니다.")
@DecimalMin(value = "33.1", message = "위도는 최소 33.1°N 이상이어야 합니다.")
@DecimalMax(value = "38.6", message = "위도는 최대 38.6°N 이하이어야 합니다.")
Double latitude,

@NotNull(message = "경도는 필수 입력값입니다.")
@DecimalMin(value = "124.6", message = "경도는 최소 124.6°E 이상이어야 합니다.")
@DecimalMax(value = "131.9", message = "경도는 최대 131.9°E 이하이어야 합니다.")
Double longitude,

@NotNull(message = "상세 조건은 필수 입력값입니다.")
Condition condition,

@Positive(message = "도보 가능 거리는 양수여야 합니다.")
Integer walkingTime,

@Positive(message = "가격대는 양수여야 합니다.")
Integer priceRange
) {

public static record Condition(
String spotType,
List<Filter> filterList
) {

public static record Filter(
@NotNull(message = "카테고리는 필수 입력값입니다.")
String category,

@NotNull(message = "옵션 리스트는 필수 입력값입니다.")
List<String> optionList
) {

}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.acon.server.spot.api.response;

import java.util.List;

public record SpotListResponse(
List<RecommendedSpot> spotList
) {

public static record RecommendedSpot(
long id, // 장소 ID
String image, // 장소 이미지 URL
Integer matchingRate, // 취향 일치율 (Optional)
String type, // 장소 분류
String name, // 장소 이름
int walkingTime // 도보 시간
) {

}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,13 +5,16 @@
import com.acon.server.global.external.GeoCodingResponse;
import com.acon.server.global.external.NaverMapsAdapter;
import com.acon.server.member.infra.repository.GuidedSpotRepository;
import com.acon.server.spot.api.request.SpotListRequest;
import com.acon.server.spot.api.response.MenuListResponse;
import com.acon.server.spot.api.response.MenuResponse;
import com.acon.server.spot.api.response.SearchSpotListResponse;
import com.acon.server.spot.api.response.SearchSpotResponse;
import com.acon.server.spot.api.response.SearchSuggestionListResponse;
import com.acon.server.spot.api.response.SearchSuggestionResponse;
import com.acon.server.spot.api.response.SpotDetailResponse;
import com.acon.server.spot.api.response.SpotListResponse;
import com.acon.server.spot.api.response.SpotListResponse.RecommendedSpot;
import com.acon.server.spot.application.mapper.SpotDtoMapper;
import com.acon.server.spot.application.mapper.SpotMapper;
import com.acon.server.spot.domain.entity.Spot;
Expand All @@ -27,6 +30,7 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
import java.util.Random;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
Expand Down Expand Up @@ -56,6 +60,16 @@ public class SpotService {

private final NaverMapsAdapter naverMapsAdapter;

private static final List<String> IMAGE_URLS = List.of(
"https://github.com/user-attachments/assets/52e88bff-2577-4f0e-ae98-9636f88afd2a",
"https://github.com/user-attachments/assets/3b0a61cc-7b96-45c1-ab4f-0b1b38597f4b",
"https://github.com/user-attachments/assets/6e4a4cad-7467-4662-83e7-3ada27c1e6e6",
"https://github.com/user-attachments/assets/5ce1fdc3-178f-4d54-bb00-4db97afa6b93",
"https://github.com/user-attachments/assets/870990b0-b414-496d-99b9-744a85ac4c9c",
"https://github.com/user-attachments/assets/52e15f07-4770-4f35-a6e5-65cedc737251"
);
private static final Random RANDOM = new Random();

// 메서드 설명: 위치 정보가 없는 Spot들의 위치 정보를 업데이트한다.
@Transactional
public void updateNullCoordinatesForSpots() {
Expand Down Expand Up @@ -95,6 +109,48 @@ private void updateSpotCoordinate(final Spot spot) {
);
}

@Transactional(readOnly = true)
public SpotListResponse fetchRecommendedSpotList(SpotListRequest request) {
// 예시: 랜덤으로 6개 Spot 도메인 엔티티 가져오기
List<SpotEntity> randomSpots = spotRepository.findRandomSpots(6);

// Spot 도메인 엔티티를 SpotListResponse.RecommendedSpot으로 변환
List<RecommendedSpot> spotList = randomSpots.stream()
.map(this::toRecommendedSpot)
.collect(Collectors.toList());

return new SpotListResponse(spotList);
}

// Spot -> RecommendedSpot 변환 메서드
private RecommendedSpot toRecommendedSpot(SpotEntity spotEntity) {
return new RecommendedSpot(
spotEntity.getId(),
fetchSpotImage(spotEntity.getId()),
generateRandomMatchingRate(), // 80~100 사이의 랜덤 값 설정
spotEntity.getSpotType().name(),
spotEntity.getName(),
calculateWalkingTime(spotEntity.getLatitude(), spotEntity.getLongitude())
);
}

private String fetchSpotImage(Long spotId) {
return spotImageRepository.findTopBySpotId(spotId)
.map(SpotImageEntity::getImage)
.orElse(IMAGE_URLS.get(RANDOM.nextInt(IMAGE_URLS.size())));
}

// 80~100 사이 랜덤값 생성
private int generateRandomMatchingRate() {
return (int) (Math.random() * (100 - 80 + 1)) + 80; // 80~100 사이 값
}

// 예시: 도보 시간 계산 (임의로 설정)
private int calculateWalkingTime(Double latitude, Double longitude) {
// 도보 시간 계산 로직 (예시로 3~10분 사이 랜덤 값 반환)
return (int) (Math.random() * (10 - 3 + 1)) + 3;
}

// TODO: 장소 추천 시 메뉴 가격 변동이면 메인 메뉴 X 처리

// TODO: 트랜잭션 범위 고민하기
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

import com.acon.server.spot.infra.entity.SpotImageEntity;
import java.util.List;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface SpotImageRepository extends JpaRepository<SpotImageEntity, Long> {

List<SpotImageEntity> findAllBySpotId(Long spotId);

Optional<SpotImageEntity> findTopBySpotId(Long spotId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ default SpotEntity findByIdOrElseThrow(Long id) {
);
}

@Query(value = "SELECT * FROM spot ORDER BY RANDOM() LIMIT :limit", nativeQuery = true)
List<SpotEntity> findRandomSpots(int limit);

@Query(value = """
SELECT s.id, s.name
FROM spot s
Expand Down

0 comments on commit 5b92095

Please sign in to comment.