Skip to content

Commit

Permalink
merge: pull request #63 from SOPT-all/feat/#57
Browse files Browse the repository at this point in the history
[FEAT/#57] 위치 기반 장소 추천 알고리즘 구현
  • Loading branch information
ckkim817 authored Jan 24, 2025
2 parents 795d34a + 21c5756 commit abdd001
Show file tree
Hide file tree
Showing 21 changed files with 586 additions and 111 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,10 @@ public void isPrincipalNull(
throw new BusinessException(ErrorType.EMPTY_PRINCIPAL_ERROR);
}
}

public boolean isGuestUser() {
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();

return principal.toString().equals(ANONYMOUS_USER);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -59,7 +59,7 @@ private String getJwtFromRequest(HttpServletRequest request) {
String bearerToken = request.getHeader("Authorization");

if (!StringUtils.hasText(bearerToken)) {
throw new BusinessException(ErrorType.UN_LOGIN_ERROR);
// throw new BusinessException(ErrorType.UN_LOGIN_ERROR);
} else if (StringUtils.hasText(bearerToken) && bearerToken.startsWith("Bearer ")) {
return bearerToken.substring("Bearer ".length());
} else if (StringUtils.hasText(bearerToken) && !bearerToken.startsWith("Bearer ")) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,14 +15,18 @@
import com.acon.server.member.domain.enums.SpotStyle;
import com.acon.server.spot.domain.enums.SpotType;
import jakarta.validation.Valid;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
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;

@RestController
Expand Down Expand Up @@ -59,6 +63,20 @@ public ResponseEntity<MemberAreaResponse> postArea(
return ResponseEntity.ok(new MemberAreaResponse(area));
}

@GetMapping(path = "/area", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<MemberAreaResponse> getArea(
@DecimalMin(value = "33.1", message = "위도는 최소 33.1°N 이상이어야 합니다.(대한민국 기준)")
@DecimalMax(value = "38.6", message = "위도는 최대 38.6°N 이하이어야 합니다.(대한민국 기준)")
@Validated @RequestParam(name = "latitude") final Double latitude,
@DecimalMin(value = "124.6", message = "경도는 최소 124.6°E 이상이어야 합니다.(대한민국 기준)")
@DecimalMax(value = "131.9", message = "경도는 최대 131.9°E 이하이어야 합니다.(대한민국 기준)")
@Validated @RequestParam(name = "longitude") final Double longitude
) {
String area = memberService.fetchMemberArea(latitude, longitude);

return ResponseEntity.ok(new MemberAreaResponse(area));
}

@PostMapping(path = "/member/preference", consumes = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> postPreference(
@Valid @RequestBody final PreferenceRequest request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,14 @@ public String createMemberArea(
return legalDong;
}

@Transactional(readOnly = true)
public String fetchMemberArea(
final Double latitude,
final Double longitude
) {
return naverMapsAdapter.getReverseGeoCodingResult(latitude, longitude);
}

@Transactional
public void createPreference(
final List<DislikeFood> dislikeFoodList,
Expand All @@ -172,11 +180,16 @@ public void createPreference(

@Transactional
public void createGuidedSpot(final Long spotId) {
if (principalHandler.isGuestUser()) {
return;
}

if (!spotRepository.existsById(spotId)) {
throw new BusinessException(ErrorType.NOT_FOUND_SPOT_ERROR);
}

MemberEntity memberEntity = memberRepository.findByIdOrElseThrow(principalHandler.getUserIdFromPrincipal());

Optional<GuidedSpotEntity> optionalGuidedSpotEntity =
guidedSpotRepository.findByMemberIdAndSpotId(memberEntity.getId(), spotId);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,4 +37,12 @@ public static Cuisine fromValue(String value) {

return cuisine;
}

public static Cuisine matchCuisine(String name) {
try {
return Cuisine.fromValue(name);
} catch (Exception e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -35,4 +35,12 @@ public static FavoriteSpot fromValue(String value) {

return favoriteSpot;
}

public static FavoriteSpot matchFavoriteSpot(String name) {
try {
return FavoriteSpot.fromValue(name);
} catch (Exception e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum SpotStyle {

TRADITIONAL,
VINTAGE,
MODERN,
;

Expand All @@ -33,4 +33,12 @@ public static SpotStyle fromValue(String value) {

return spotStyle;
}

public static SpotStyle matchSpotStyle(String name) {
try {
return SpotStyle.fromValue(name);
} catch (Exception e) {
return null;
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package com.acon.server.member.infra.repository;

import com.acon.server.spot.api.response.SearchSuggestionResponse;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jakarta.persistence.Query;
import java.util.List;
import org.springframework.stereotype.Repository;

@Repository
public class GuidedSpotCustomRepository {

@PersistenceContext
private EntityManager em;

public List<SearchSuggestionResponse> findRecentGuidedSpotSuggestions(
Long memberId,
double lat,
double lon,
double range,
int limit
) {
String sql = """
SELECT s.id AS spot_id,
s.name AS spot_name
FROM guided_spot gs
JOIN spot s ON s.id = gs.spot_id
WHERE gs.member_id = :memberId
AND ST_DWithin(
s.geom::geography,
ST_SetSRID(ST_MakePoint(:lon, :lat), 4326)::geography,
:range
)
ORDER BY gs.updated_at DESC
LIMIT :limit
""";

Query nativeQuery = em.createNativeQuery(sql, "SearchSuggestionResponseMapping");
nativeQuery.setParameter("memberId", memberId);
nativeQuery.setParameter("lat", lat);
nativeQuery.setParameter("lon", lon);
nativeQuery.setParameter("range", range);
nativeQuery.setParameter("limit", limit);

@SuppressWarnings("unchecked")
List<SearchSuggestionResponse> result = nativeQuery.getResultList();

return result;
}
}
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
package com.acon.server.member.infra.repository;

import com.acon.server.member.infra.entity.PreferenceEntity;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;

public interface PreferenceRepository extends JpaRepository<PreferenceEntity, Long> {

Optional<PreferenceEntity> findByMemberId(Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,6 @@ public class SpotController {

private final SpotService spotService;

// 위치 및 사용자 온보딩 결과 기반 개인화 장소 리스트 추천 API 컨트롤러 메서드
@PostMapping(
path = "/spots",
consumes = MediaType.APPLICATION_JSON_VALUE,
Expand Down
Empty file.
33 changes: 15 additions & 18 deletions src/main/java/com/acon/server/spot/api/request/SpotListRequest.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,35 +7,32 @@
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 이하이어야 합니다.")
@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 이하이어야 합니다.")
@NotNull(message = "경도는 필수입니다.")
@DecimalMin(value = "124.6", message = "경도는 최소 124.6°E 이상이어야 합니다.(대한민국 기준)")
@DecimalMax(value = "131.9", message = "경도는 최대 131.9°E 이하이어야 합니다.(대한민국 기준)")
Double longitude,

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

public static record Condition(
public record Condition(
String spotType,
List<Filter> filterList,
@Positive(message = "가격대는 양수여야 합니다.")
Integer priceRange,

@NotNull(message = "도보 가능 거리는 필수입니다.")
@Positive(message = "도보 가능 거리는 양수여야 합니다.")
Integer walkingTime
Integer walkingTime,
@Positive(message = "가격대는 양수여야 합니다.")
Integer priceRange
) {

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

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

Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,15 @@
package com.acon.server.spot.api.response;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonInclude.Include;
import java.util.List;

@JsonInclude(Include.NON_NULL)
public record SpotListResponse(
List<RecommendedSpot> spotList
) {

public static record RecommendedSpot(
public record RecommendedSpot(
long id, // 장소 ID
String image, // 장소 이미지 URL
Integer matchingRate, // 취향 일치율 (Optional)
Expand Down
Loading

0 comments on commit abdd001

Please sign in to comment.