Skip to content

Commit

Permalink
merge: pull request #43 from SOPT-all/feat/#37
Browse files Browse the repository at this point in the history
[FEAT/#37] 추천 검색어 조회 API 구현
  • Loading branch information
ckkim817 authored Jan 21, 2025
2 parents 32107e4 + b6e6838 commit 58c81f9
Show file tree
Hide file tree
Showing 11 changed files with 173 additions and 20 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ public ResponseEntity<MemberAreaResponse> postArea(
return ResponseEntity.ok(new MemberAreaResponse(area));
}

// TODO: Member 도메인에 있어야 할까? 고민 필요
@PostMapping(path = "/member/guided-spot", produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Void> postGuidedSpot(
@Valid @RequestBody final GuidedSpotRequest request
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -51,9 +51,9 @@ public class MemberService {
private final VerifiedAreaRepository verifiedAreaRepository;
private final SpotRepository spotRepository;

private final GuidedSpotMapper guidedSpotMapper;
private final MemberMapper memberMapper;
private final PreferenceMapper preferenceMapper;
private final GuidedSpotMapper guidedSpotMapper;

private final JwtUtils jwtUtils;
private final GoogleSocialService googleSocialService;
Expand Down Expand Up @@ -142,7 +142,6 @@ public void createGuidedSpot(final Long spotId, final Long memberId) {
.build()
)
);

}

public String createMemberArea(final Double latitude, final Double longitude, final Long memberId) {
Expand Down Expand Up @@ -176,4 +175,6 @@ private String extractAreaName(Map<String, Object> response) {
}
throw new BusinessException(ErrorType.INTERNAL_SERVER_ERROR);
}

// TODO: 최근 길 안내 장소 지우는 스케줄러 추가
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,4 +7,6 @@
public interface GuidedSpotRepository extends JpaRepository<GuidedSpotEntity, Long> {

Optional<GuidedSpotEntity> findByMemberIdAndSpotId(Long memberId, Long spotId);

Optional<GuidedSpotEntity> findTopByMemberIdOrderByUpdatedAtDesc(Long memberId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,9 +2,12 @@

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.VerifiedSpotResponse;
import com.acon.server.spot.application.service.SpotService;
import jakarta.validation.constraints.DecimalMax;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.Positive;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand Down Expand Up @@ -42,6 +45,20 @@ public ResponseEntity<MenuListResponse> getMenus(
);
}

@GetMapping("/search-suggestions")
public ResponseEntity<SearchSuggestionListResponse> getSearchSuggestions(
@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
) {
return ResponseEntity.ok(
spotService.fetchSearchSuggestions(latitude, longitude)
);
}

// TODO: 메서드 네이밍 수정 필요
@GetMapping("/spots/search")
public ResponseEntity<SearchSpotListResponse> searchSpot(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.acon.server.spot.api.response;

import java.util.List;

public record SearchSuggestionListResponse(
List<SearchSuggestionResponse> suggestionList
) {

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

public record SearchSuggestionResponse(
Long spotId,
String spotName
) {

}
Original file line number Diff line number Diff line change
@@ -1,13 +1,19 @@
package com.acon.server.spot.application.mapper;

import com.acon.server.spot.api.response.SearchSuggestionResponse;
import com.acon.server.spot.api.response.SpotDetailResponse;
import com.acon.server.spot.infra.entity.SpotEntity;
import java.util.List;
import org.mapstruct.Mapper;
import org.mapstruct.Mapping;
import org.mapstruct.ReportingPolicy;

@Mapper(componentModel = "spring", unmappedTargetPolicy = ReportingPolicy.ERROR)
public interface SpotDtoMapper {

SpotDetailResponse toSpotDetailResponse(SpotEntity spotEntity, List<String> imageList, boolean openStatus);

@Mapping(target = "spotId", source = "id")
@Mapping(target = "spotName", source = "name")
SearchSuggestionResponse toSearchSuggestionResponse(SpotEntity spotEntity);
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,13 @@
import com.acon.server.global.exception.ErrorType;
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.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.application.mapper.SpotDtoMapper;
import com.acon.server.spot.application.mapper.SpotMapper;
Expand All @@ -24,6 +27,9 @@
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
import java.util.stream.Stream;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
Expand All @@ -34,12 +40,15 @@
@Slf4j
public class SpotService {

private final static int DISTANCE_RANGE = 250;
private static final int WALKING_RADIUS_30_MIN = 2000;
private static final int SUGGESTION_LIMIT = 5;
private static final int VERIFICATION_DISTANCE = 250;

private final SpotRepository spotRepository;
private final MenuRepository menuRepository;
private final OpeningHourRepository openingHourRepository;
private final SpotImageRepository spotImageRepository;
private final GuidedSpotRepository guidedSpotRepository;

private final SpotDtoMapper spotDtoMapper;
private final SpotMapper spotMapper;
Expand All @@ -58,16 +67,16 @@ public void updateNullCoordinatesForSpots() {

log.info("위도 또는 경도 정보가 비어 있는 Spot 데이터를 {}건 찾았습니다.", spotEntityList.size());

List<SpotEntity> updatedEntities = spotEntityList.stream()
List<SpotEntity> updatedEntityList = spotEntityList.stream()
.map(spotEntity -> {
Spot spot = spotMapper.toDomain(spotEntity);
updateSpotCoordinate(spot);
return spotMapper.toEntity(spot);
})
.toList();
spotRepository.saveAll(updatedEntities);
spotRepository.saveAll(updatedEntityList);

log.info("위도 또는 경도 정보가 비어 있는 Spot 데이터 {}건을 업데이트 했습니다.", updatedEntities.size());
log.info("위도 또는 경도 정보가 비어 있는 Spot 데이터 {}건을 업데이트 했습니다.", updatedEntityList.size());
}

// 메서드 설명: spotId에 해당하는 Spot의 좌표를 업데이트한다. (주소 -> 좌표)
Expand All @@ -78,8 +87,11 @@ private void updateSpotCoordinate(final Spot spot) {
Double.parseDouble(geoCodingResponse.latitude()),
Double.parseDouble(geoCodingResponse.longitude())
);
spot.updateLocation();
}

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

// TODO: 트랜잭션 범위 고민하기
// 메서드 설명: spotId에 해당하는 Spot의 상세 정보를 조회한다. (메뉴, 이미지, 영업 여부 등)
@Transactional
Expand Down Expand Up @@ -164,8 +176,56 @@ public MenuListResponse fetchMenus(final Long spotId) {
return new MenuListResponse(menuResponseList);
}

@Transactional(readOnly = true)
public SearchSuggestionListResponse fetchSearchSuggestions(final Double latitude, final Double longitude) {
// TODO: 토큰 검증 이후 MemberID 추출 로직 필요
List<SearchSuggestionResponse> recentSpotSuggestion =
guidedSpotRepository.findTopByMemberIdOrderByUpdatedAtDesc(1L)
.flatMap(recentGuidedSpot -> spotRepository.findById(recentGuidedSpot.getSpotId()))
.map(spotDtoMapper::toSearchSuggestionResponse)
.stream()
.toList();

List<SearchSuggestionResponse> nearestSpotList =
findNearestSpotList(longitude, latitude, WALKING_RADIUS_30_MIN, SUGGESTION_LIMIT);

// Set을 통한 필터링 성능 향상
Set<Long> recentSpotIds = recentSpotSuggestion.stream()
.map(SearchSuggestionResponse::spotId)
.collect(Collectors.toSet());

List<SearchSuggestionResponse> filteredNearestSpotList = nearestSpotList.stream()
.filter(nearestSpot -> !recentSpotIds.contains(nearestSpot.spotId()))
.toList();

List<SearchSuggestionResponse> combinedSuggestionList = Stream.concat(
recentSpotSuggestion.stream(),
filteredNearestSpotList.stream()
)
.limit(SUGGESTION_LIMIT)
.toList();

return new SearchSuggestionListResponse(combinedSuggestionList);
}

// TODO: limit 없는 메서드로부터 분기하도록 리팩토링
private List<SearchSuggestionResponse> findNearestSpotList(
double longitude,
double latitude,
double radius,
int limit
) {
List<Object[]> rawFindResults = spotRepository.findNearestSpotList(longitude, latitude, radius, limit);

return rawFindResults.stream()
.map(result -> new SearchSuggestionResponse((Long) result[0], (String) result[1]))
.toList();
}

public SearchSpotListResponse searchSpot(final String keyword) {
List<SpotEntity> spotEntityList = spotRepository.findTop10ByNameContainsIgnoreCase(keyword);

// TODO: mapper로 변경
List<SearchSpotResponse> spotList = spotEntityList.stream()
.map(spotEntity -> SearchSpotResponse.builder()
.spotId(spotEntity.getId())
Expand All @@ -180,12 +240,13 @@ public SearchSpotListResponse searchSpot(final String keyword) {

@Transactional(readOnly = true)
public boolean verifySpot(Long spotId, Double memberLongitude, Double memberLatitude) {
SpotEntity spotEntity = spotRepository.findByIdOrThrow(spotId);
Double spotLongitude = spotEntity.getLongitude();
Double spotLatitude = spotEntity.getLatitude();
Double distance = spotRepository.calculateDistance(memberLongitude, memberLatitude, spotLongitude,
spotLatitude);
if (!spotRepository.existsById(spotId)) {
throw new BusinessException(ErrorType.NOT_FOUND_SPOT_ERROR);
}

Double distance =
spotRepository.calculateDistanceFromSpot(spotId, memberLongitude, memberLatitude);

return distance < DISTANCE_RANGE;
return distance < VERIFICATION_DISTANCE;
}
}
15 changes: 15 additions & 0 deletions src/main/java/com/acon/server/spot/domain/entity/Spot.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,11 +5,16 @@
import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import org.locationtech.jts.geom.Coordinate;
import org.locationtech.jts.geom.GeometryFactory;
import org.locationtech.jts.geom.Point;

@Getter
@ToString
public class Spot {

private static final GeometryFactory geometryFactory = new GeometryFactory();

private final Long id;
private final String name;
private final SpotType spotType;
Expand All @@ -21,6 +26,7 @@ public class Spot {
private LocalDateTime basicAcornUpdatedAt;
private Double latitude;
private Double longitude;
private Point geom;
private String adminDong;
private LocalDateTime createdAt;
private LocalDateTime updatedAt;
Expand All @@ -37,6 +43,7 @@ public Spot(
LocalDateTime basicAcornUpdatedAt,
Double latitude,
Double longitude,
Point geom,
String adminDong,
LocalDateTime createdAt,
LocalDateTime updatedAt
Expand All @@ -51,6 +58,7 @@ public Spot(
this.basicAcornUpdatedAt = basicAcornUpdatedAt;
this.latitude = latitude;
this.longitude = longitude;
this.geom = geom;
this.adminDong = adminDong;
this.createdAt = createdAt;
this.updatedAt = updatedAt;
Expand All @@ -60,4 +68,11 @@ public void updateCoordinate(Double latitude, Double longitude) {
this.latitude = latitude;
this.longitude = longitude;
}

public void updateLocation() {
if (latitude != null && longitude != null) {
this.geom = geometryFactory.createPoint(new Coordinate(longitude, latitude));
this.geom.setSRID(4326);
}
}
}
10 changes: 10 additions & 0 deletions src/main/java/com/acon/server/spot/infra/entity/SpotEntity.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,11 +15,15 @@
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;
import org.locationtech.jts.geom.Point;

@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Table(name = "spot")
// TODO: 공간 인덱스 설정
public class SpotEntity extends BaseTimeEntity {

@Id
Expand Down Expand Up @@ -54,6 +58,10 @@ public class SpotEntity extends BaseTimeEntity {
@Column(name = "longitude")
private Double longitude;

@JdbcTypeCode(SqlTypes.GEOMETRY)
@Column(name = "geom", columnDefinition = "geometry(Point, 4326)")
private Point geom;

@Column(name = "admin_dong")
private String adminDong;

Expand All @@ -69,6 +77,7 @@ public SpotEntity(
String address,
Double latitude,
Double longitude,
Point geom,
String adminDong
) {
this.id = id;
Expand All @@ -82,6 +91,7 @@ public SpotEntity(
this.address = address;
this.latitude = latitude;
this.longitude = longitude;
this.geom = geom;
this.adminDong = adminDong;
}
}
Loading

0 comments on commit 58c81f9

Please sign in to comment.