diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java index 32093ff7..432dd1d0 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostForUserService.java @@ -107,18 +107,21 @@ public GetPostForUserResponse findById(Long postId, Long userId) { return studentCouncilPostMapper.toGetPostForUserResponse(post, imageUrls, userId, isLiked); } - public Page findSchoolPosts(PostCategory category, int page, int size, Long userId, Long excludePostId) { + public Page findSchoolPosts(PostCategory category, int page, int size, Long userId, + Long excludePostId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); Pageable pageable = PageRequest.of(Math.max(page - 1, 0), size, Sort.by(Sort.Direction.DESC, "startDateTime")); Page posts = studentCouncilPostRepository - .findBySchoolId(user.getSchool().getSchoolId(), category, CouncilType.SCHOOL_COUNCIL, excludePostId, pageable); + .findBySchoolId(user.getSchool().getSchoolId(), category, CouncilType.SCHOOL_COUNCIL, excludePostId, + pageable); return mapPostsWithLikes(posts, userId); } - public Page findCollegePosts(PostCategory category, int page, int size, Long userId, Long excludePostId) { + public Page findCollegePosts(PostCategory category, int page, int size, Long userId, + Long excludePostId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); if (user.isProfileNotCompleted() || user.getCollege() == null) { @@ -128,12 +131,14 @@ public Page findCollegePosts(PostCategory category, int pa Pageable pageable = PageRequest.of(Math.max(page - 1, 0), size, Sort.by(Sort.Direction.DESC, "startDateTime")); Page posts = studentCouncilPostRepository - .findByCollegeId(user.getCollege().getCollegeId(), category, CouncilType.COLLEGE_COUNCIL, excludePostId, pageable); + .findByCollegeId(user.getCollege().getCollegeId(), category, CouncilType.COLLEGE_COUNCIL, excludePostId, + pageable); return mapPostsWithLikes(posts, userId); } - public Page findMajorPosts(PostCategory category, int page, int size, Long userId, Long excludePostId) { + public Page findMajorPosts(PostCategory category, int page, int size, Long userId, + Long excludePostId) { User user = userRepository.findByIdWithAcademicInfo(userId).orElseThrow(UserNotFoundException::new); if (user.isProfileNotCompleted() || user.getMajor() == null) { diff --git a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java index 3dfc7864..5059d5a0 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java +++ b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/StudentCouncilPostRepository.java @@ -14,6 +14,7 @@ import com.campus.campus.domain.council.domain.entity.CouncilType; import com.campus.campus.domain.councilpost.domain.entity.PostCategory; import com.campus.campus.domain.councilpost.domain.entity.StudentCouncilPost; +import com.campus.campus.domain.councilpost.domain.entity.ThumbnailIcon; public interface StudentCouncilPostRepository extends JpaRepository { @@ -313,4 +314,32 @@ List findTop3RecommendedPartnershipPlaces( Pageable pageable ); + + @EntityGraph(attributePaths = {"writer", "writer.school", "writer.college", "writer.major", "place"}) + @Query(""" + SELECT p FROM StudentCouncilPost p + JOIN p.writer w + JOIN p.place pl + LEFT JOIN w.school s + LEFT JOIN w.college c + LEFT JOIN w.major m + WHERE p.category = 'PARTNERSHIP' + AND p.thumbnailIcon = :icon + AND :now BETWEEN p.startDateTime AND p.endDateTime + AND w.deletedAt IS NULL + AND ( + (w.councilType = 'SCHOOL_COUNCIL' AND s.schoolId = :schoolId) + OR (w.councilType = 'COLLEGE_COUNCIL' AND c.collegeId = :collegeId) + OR (w.councilType = 'MAJOR_COUNCIL' AND m.majorId = :majorId) + ) + ORDER BY p.id DESC + """) + List findRandomPartnershipPlace( + @Param("schoolId") Long schoolId, + @Param("collegeId") Long collegeId, + @Param("majorId") Long majorId, + @Param("icon") ThumbnailIcon icon, + @Param("now") LocalDateTime now, + Pageable pageable + ); } diff --git a/src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendNearByPlaceResponse.java b/src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendNearByPlaceResponse.java new file mode 100644 index 00000000..6d38c3da --- /dev/null +++ b/src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendNearByPlaceResponse.java @@ -0,0 +1,39 @@ +package com.campus.campus.domain.place.application.dto.response; + +import java.util.List; + +import com.campus.campus.domain.place.domain.entity.Coordinate; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; + +public record RecommendNearByPlaceResponse( + @Schema(description = "해당 장소명", example = "숙명여자대학교") + @NotBlank + String placeName, + + @Schema(description = "장소 식별 고유 ID") + @NotBlank + String placeKey, + + @Schema(description = "장소 주소", example = "서울특별시 용산구 청파로47길 99") + @NotBlank + String address, + + @Schema(description = "장소 카테고리", example = "교육,학문>대학교") + @NotBlank + String category, + + @Schema(description = "장소 상세 정보 네이버 페이지 하이퍼링크", example = "https://map.naver.com/v5/search/%EC%88%99%EB%AA%85%EC%97%AC%EC%9E%90%EB%8C%80%ED%95%99%EA%B5%90+%EC%A0%9C1%EC%BA%A0%ED%8D%BC%EC%8A%A4?c=37.545947,126.964578,15,0,0,0,dh") + String link, + + @Schema(description = "전화번호", example = "010-1234-1234") + String telephone, + + @Schema(description = "위도/경도") + Coordinate coordinate, + + @Schema(description = "이미지 url") + List imgUrls +) { +} diff --git a/src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPartnershipPlaceResponse.java b/src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPartnershipPlaceResponse.java new file mode 100644 index 00000000..d008e3e6 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPartnershipPlaceResponse.java @@ -0,0 +1,24 @@ +package com.campus.campus.domain.place.application.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record RecommendPartnershipPlaceResponse( + @Schema(description = "장소 ID", example = "10") + Long placeId, + + @Schema(description = "장소 이름", example = "봉구스밥버거 중앙대후문점") + String placeName, + + @Schema(description = "제휴한 학생회 이름", example = "가천대학교 총학생회") + String councilName, + + @Schema(description = "제휴 이름 (게시글 제목)", example = "전 메뉴 10% 할인") + String partnershipTitle, + + @Schema(description = "장소 주소", example = "서울특별시 동작구 상도1동") + String address, + + @Schema(description = "제휴 썸네일 이미지", example = "https://cdn.example.com/image.jpg") + String imageUrl +) { +} diff --git a/src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPlaceByTimeResponse.java b/src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPlaceByTimeResponse.java new file mode 100644 index 00000000..ebf77e19 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/place/application/dto/response/RecommendPlaceByTimeResponse.java @@ -0,0 +1,17 @@ +package com.campus.campus.domain.place.application.dto.response; + +import java.util.List; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record RecommendPlaceByTimeResponse( + @Schema(description = "추천 타입 (LUNCH: 점심, CAFE: 카페, NONE: 해당 시간 아님)", example = "LUNCH") + String type, + + @Schema(description = "추천 제휴 게시글 (최대 2개)") + List partnershipPosts, + + @Schema(description = "추천 주변 장소 (최대 2개)") + List nearbyPlaces +) { +} diff --git a/src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java b/src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java index d4d6b494..39bd6217 100644 --- a/src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java +++ b/src/main/java/com/campus/campus/domain/place/application/mapper/PlaceMapper.java @@ -8,6 +8,9 @@ import com.campus.campus.domain.councilpost.domain.entity.StudentCouncilPost; import com.campus.campus.domain.place.application.dto.response.PartnershipPinResponse; import com.campus.campus.domain.place.application.dto.response.LikeResponse; +import com.campus.campus.domain.place.application.dto.response.RecommendNearByPlaceResponse; +import com.campus.campus.domain.place.application.dto.response.RecommendPartnershipPlaceResponse; +import com.campus.campus.domain.place.application.dto.response.RecommendPlaceByTimeResponse; import com.campus.campus.domain.place.application.dto.response.SavedPlaceInfo; import com.campus.campus.domain.place.application.dto.response.naver.NaverSearchResponse; import com.campus.campus.domain.place.application.dto.response.partnership.PartnershipResponse; @@ -69,6 +72,37 @@ public PartnershipResponse toPartnershipResponse(User user, StudentCouncilPost p ); } + public RecommendPlaceByTimeResponse toRecommendPlaceByTimeResponse(String type, + List partnershipPosts, List nearbyPlaces) { + + return new RecommendPlaceByTimeResponse(type, partnershipPosts, nearbyPlaces); + } + + public RecommendPartnershipPlaceResponse toRecommendPartnershipPlaceResponse(StudentCouncilPost post) { + return new RecommendPartnershipPlaceResponse( + post.getPlace().getPlaceId(), + post.getPlace().getPlaceName(), + post.getWriter().getCouncilName(), + post.getTitle(), + post.getPlace().getAddress(), + post.getThumbnailImageUrl() + ); + } + + public RecommendNearByPlaceResponse toRecommendNearByPlaceResponse(SavedPlaceInfo savedPlaceInfo, + List imageUrl) { + return new RecommendNearByPlaceResponse( + savedPlaceInfo.placeName(), + savedPlaceInfo.placeKey(), + savedPlaceInfo.address(), + savedPlaceInfo.category(), + savedPlaceInfo.link(), + savedPlaceInfo.telephone(), + savedPlaceInfo.coordinate(), + imageUrl + ); + } + private String resolveTag(StudentCouncilPost post, User user) { CouncilType councilType = post.getWriter().getCouncilType(); return switch (councilType) { diff --git a/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java b/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java index 5d3b2cec..4b1fbff4 100644 --- a/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java +++ b/src/main/java/com/campus/campus/domain/place/application/service/PlaceService.java @@ -2,6 +2,11 @@ import java.net.URLEncoder; import java.nio.charset.StandardCharsets; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.time.ZoneId; +import java.util.ArrayList; +import java.util.Collections; import java.util.List; import java.util.Map; import java.util.Optional; @@ -11,10 +16,18 @@ import java.util.stream.Collectors; import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.data.domain.PageRequest; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import com.campus.campus.domain.councilpost.application.exception.AcademicInfoNotSetException; +import com.campus.campus.domain.councilpost.domain.entity.StudentCouncilPost; +import com.campus.campus.domain.councilpost.domain.entity.ThumbnailIcon; +import com.campus.campus.domain.councilpost.domain.repository.StudentCouncilPostRepository; import com.campus.campus.domain.place.application.dto.response.LikeResponse; +import com.campus.campus.domain.place.application.dto.response.RecommendNearByPlaceResponse; +import com.campus.campus.domain.place.application.dto.response.RecommendPartnershipPlaceResponse; +import com.campus.campus.domain.place.application.dto.response.RecommendPlaceByTimeResponse; import com.campus.campus.domain.place.application.dto.response.SavedPlaceInfo; import com.campus.campus.domain.place.application.dto.response.SearchCandidateResponse; import com.campus.campus.domain.place.application.dto.response.geocoder.AddressResponse; @@ -48,18 +61,30 @@ @Slf4j public class PlaceService { + private static final LocalTime LUNCH_START = LocalTime.of(11, 30); + private static final LocalTime LUNCH_END = LocalTime.of(14, 0); + private static final LocalTime CAFE_START = LocalTime.of(14, 0); + private static final LocalTime CAFE_END = LocalTime.of(17, 0); + private static final LocalTime DINNER_START = LocalTime.of(17, 0); + private static final LocalTime DINNER_END = LocalTime.of(20, 0); + private static final LocalTime BAR_START = LocalTime.of(20, 0); + private static final LocalTime BAR_END = LocalTime.of(23, 30); + private static final ZoneId KST = ZoneId.of("Asia/Seoul"); + private final NaverMapClient naverMapClient; private final PlaceMapper placeMapper; private final PlaceRepository placeRepository; private final GooglePlaceClient googleClient; + private final StudentCouncilPostRepository studentCouncilPostRepository; private final PlaceImagesRepository placeImagesRepository; private final PresignedUrlService presignedUrlService; + private final RedisPlaceCacheService redisPlaceCacheService; private final LikedPlacesRepository likedPlacesRepository; private final UserRepository userRepository; private final ExecutorService executorService; private final GeoCoderClient geoCoderClient; - public List searchByLocationAndKeyword(double lat, double lng, String keyword) { + public List searchByLocationAndKeyword(double lat, double lng, String keyword, int imageLimit) { String searchWord = keyword; try { @@ -79,15 +104,15 @@ public List searchByLocationAndKeyword(double lat, double lng, S //네이버에서 특정 장소 기본정보 받아오기 NaverSearchResponse naverSearchResponse = naverMapClient.searchPlaces(searchWord, 5); - return processSearchResults(naverSearchResponse); + return processSearchResults(naverSearchResponse, imageLimit); } - public List searchByKeyword(String keyword) { + public List searchByKeyword(String keyword, int imageLimit) { NaverSearchResponse naverSearchResponse = naverMapClient.searchPlaces(keyword, 5); - return processSearchResults(naverSearchResponse); + return processSearchResults(naverSearchResponse, imageLimit); } - + public Place findOrCreatePlace(SavedPlaceInfo place) { String placeKey = place.placeKey(); @@ -150,7 +175,30 @@ public LikeResponse likePlace(SavedPlaceInfo placeInfo, Long userId) { return placeMapper.toLikeResponse(place); } - private List processSearchResults(NaverSearchResponse naverSearchResponse) { + @Transactional(readOnly = true) + public RecommendPlaceByTimeResponse findRecommendations(Long userId, double lat, double lng) { + LocalTime now = LocalTime.now(KST); + User user = userRepository.findByIdAndDeletedAtIsNull(userId) + .orElseThrow(UserNotFoundException::new); + + if (user.isProfileNotCompleted()) { + throw new AcademicInfoNotSetException(); + } + + if (isLunchTime(now)) { + return generateResponse(user, lat, lng, ThumbnailIcon.FOOD, "식당", "LUNCH"); + } else if (isCafeTime(now)) { + return generateResponse(user, lat, lng, ThumbnailIcon.CAFE, "카페", "CAFE"); + } else if (isDinnerTime(now)) { + return generateResponse(user, lat, lng, ThumbnailIcon.FOOD, "식당", "DINNER"); + } else if (isBarTime(now)) { + return generateResponse(user, lat, lng, ThumbnailIcon.BAR, "술집", "BAR"); + } else { + return placeMapper.toRecommendPlaceByTimeResponse("잠잘시간입니다.", List.of(), List.of()); + } + } + + private List processSearchResults(NaverSearchResponse naverSearchResponse, int imageLimit) { List candidates = naverSearchResponse.items().stream() .map(item -> { String name = stripHtml(item.title()); @@ -173,7 +221,7 @@ private List processSearchResults(NaverSearchResponse naverSearc )); List> futures = candidates.stream() - .map(response -> CompletableFuture.supplyAsync(() -> convertToSavedPlaceInfo(response, images), + .map(response -> CompletableFuture.supplyAsync(() -> convertToSavedPlaceInfo(response, images, imageLimit), executorService) .completeOnTimeout(fallback(response), 4, TimeUnit.SECONDS) .exceptionally(ex -> fallback(response))) @@ -183,10 +231,11 @@ private List processSearchResults(NaverSearchResponse naverSearc return futures.stream().map(CompletableFuture::join).toList(); } - private SavedPlaceInfo convertToSavedPlaceInfo(SearchCandidateResponse response, Map> images) { + private SavedPlaceInfo convertToSavedPlaceInfo(SearchCandidateResponse response, Map> images, + int imageLimit) { List cached = images.getOrDefault(response.placeKey(), List.of()); List placeImages = !cached.isEmpty() - ? cached : googleClient.fetchImages(response.name(), response.address(), 3); + ? cached : googleClient.fetchImages(response.name(), response.address(), imageLimit); return placeMapper.toSavedPlaceInfo(response.item(), response.name(), response.placeKey(), response.naverPlaceUrl(), placeImages == null ? List.of() : placeImages @@ -272,4 +321,85 @@ private String toStringAddress(AddressResponse nowAddress) { .orElse(null); } + private boolean isLunchTime(LocalTime now) { + return !now.isBefore(LUNCH_START) && now.isBefore(LUNCH_END); + } + + private boolean isCafeTime(LocalTime now) { + return !now.isBefore(CAFE_START) && now.isBefore(CAFE_END); + } + + private boolean isDinnerTime(LocalTime now) { + return !now.isBefore(DINNER_START) && now.isBefore(DINNER_END); + } + + private boolean isBarTime(LocalTime now) { + return !now.isBefore(BAR_START) && now.isBefore(BAR_END); + } + + private RecommendPlaceByTimeResponse generateResponse(User user, double lat, double lng, ThumbnailIcon icon, + String keyword, String type) { + List partnerships = getRandomPartnerships(user, icon); + + List externalPlaces = getRandomNearByPlaces(lat, lng, keyword); + + return placeMapper.toRecommendPlaceByTimeResponse(type, partnerships, externalPlaces); + } + + private List getRandomPartnerships(User user, ThumbnailIcon icon) { + Long schoolId = user.getSchool().getSchoolId(); + Long collegeId = user.getCollege() != null ? user.getCollege().getCollegeId() : null; + Long majorId = user.getMajor() != null ? user.getMajor().getMajorId() : null; + + int poolSize = 15; + List posts = studentCouncilPostRepository.findRandomPartnershipPlace( + schoolId, collegeId, majorId, icon, LocalDateTime.now(KST), PageRequest.of(0, poolSize) + ); + + if (posts.isEmpty()) { + return List.of(); + } + + List mutablePosts = new ArrayList<>(posts); + Collections.shuffle(mutablePosts); + + return mutablePosts.stream() + .limit(2) + .map(placeMapper::toRecommendPartnershipPlaceResponse) + .toList(); + } + + private List getRandomNearByPlaces(double lat, double lng, String keyword) { + Optional> cachedPlaces = redisPlaceCacheService.getCachedPlaces(lat, lng, keyword); + + List searchResults; + + if (cachedPlaces.isPresent()) { + searchResults = cachedPlaces.get(); + } else { + searchResults = searchByLocationAndKeyword(lat, lng, keyword, 1); + if (!searchResults.isEmpty()) { + redisPlaceCacheService.cachePlaces(keyword, lat, lng, searchResults); + } + } + + if (searchResults.isEmpty()) { + return List.of(); + } + + List mutableList = new ArrayList<>(searchResults); + Collections.shuffle(mutableList); + + return mutableList.stream() + .limit(2) + .map(info -> { + List imageUrl = (info.imgUrls() != null && !info.imgUrls().isEmpty()) + ? List.of(info.imgUrls().get(0)) + : Collections.emptyList(); + + return placeMapper.toRecommendNearByPlaceResponse(info, imageUrl); + }) + .toList(); + } + } diff --git a/src/main/java/com/campus/campus/domain/place/application/service/RedisPlaceCacheService.java b/src/main/java/com/campus/campus/domain/place/application/service/RedisPlaceCacheService.java new file mode 100644 index 00000000..8e124a91 --- /dev/null +++ b/src/main/java/com/campus/campus/domain/place/application/service/RedisPlaceCacheService.java @@ -0,0 +1,68 @@ +package com.campus.campus.domain.place.application.service; + +import java.time.Duration; +import java.util.List; +import java.util.Optional; + +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.stereotype.Service; + +import com.campus.campus.domain.place.application.dto.response.SavedPlaceInfo; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.ObjectMapper; + +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Service +@RequiredArgsConstructor +@Slf4j +public class RedisPlaceCacheService { + private final RedisTemplate redisTemplate; + private final ObjectMapper objectMapper; + + private static final Duration CACHE_TTL = Duration.ofHours(1); + private static final double GRID_PRECISION = 1000.0; + + public void cachePlaces(String keyword, double lat, double lng, List places) { + if (places == null || places.isEmpty()) { + return; + } + + String key = generateKey(lat, lng, keyword); + try { + String jsonValue = objectMapper.writeValueAsString(places); + redisTemplate.opsForValue().set(key, jsonValue, CACHE_TTL); + log.info("[Redis] Cache Saved: key={}", key); + } catch (Exception e) { + log.warn("[Redis] Cache Save Failed: key={}", key, e); + } + } + + public Optional> getCachedPlaces(double lat, double lng, String keyword) { + String key = generateKey(lat, lng, keyword); + + Object value = redisTemplate.opsForValue().get(key); + if (value == null) { + return Optional.empty(); + } + + try { + String jsonValue = String.valueOf(value); + List places = objectMapper.readValue(jsonValue, new TypeReference>() { + }); + log.info("[Redis] Cache Hit: key={}", key); + return Optional.of(places); + } catch (Exception e) { + log.warn("[Redis] Cache Parsing Failed: key={}", key, e); + return Optional.empty(); + } + } + + private String generateKey(double lat, double lng, String keyword) { + double roundedLat = Math.round(lat * GRID_PRECISION) / GRID_PRECISION; + double roundedLng = Math.round(lng * GRID_PRECISION) / GRID_PRECISION; + + return String.format("places:recommend:%s:%s:%s", keyword, roundedLat, roundedLng); + } +} diff --git a/src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java b/src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java index e2453e51..c926f220 100644 --- a/src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java +++ b/src/main/java/com/campus/campus/domain/place/infrastructure/google/GooglePlaceClient.java @@ -51,7 +51,7 @@ public GooglePlaceClient( /* * 장소 이름 + 주소를 기준으로 google places에서 이미지 URL 목록을 가져옴 */ - public List fetchImages(String name, String address, int limit) { + public List fetchImages(String name, String address, int imageLimit) { boolean acquired = false; try { acquired = googleApiSemaphore.tryAcquire(5, TimeUnit.SECONDS); @@ -73,7 +73,7 @@ public List fetchImages(String name, String address, int limit) { //imageURL 생성 List imageUrls = photoRefs.stream() - .limit(3) + .limit(imageLimit) .map(this::buildPhotoUrl) .toList(); diff --git a/src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java b/src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java index 8e5fc9de..471b2f39 100644 --- a/src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java +++ b/src/main/java/com/campus/campus/domain/place/presentation/PlaceController.java @@ -11,6 +11,7 @@ import org.springframework.web.bind.annotation.RestController; import com.campus.campus.domain.place.application.dto.response.PartnershipPinResponse; +import com.campus.campus.domain.place.application.dto.response.RecommendPlaceByTimeResponse; import com.campus.campus.domain.place.application.service.PartnershipPlaceService; import com.campus.campus.domain.place.application.dto.response.LikeResponse; import com.campus.campus.domain.place.application.dto.response.SavedPlaceInfo; @@ -55,7 +56,7 @@ public CommonResponse> getPlaceInfoWithLocationAndKeyword( ) @RequestParam double lng ) { - List searchResponse = placeService.searchByLocationAndKeyword(lat, lng, keyword); + List searchResponse = placeService.searchByLocationAndKeyword(lat, lng, keyword, 3); return CommonResponse.success(PlaceResponseCode.PLACE_SEARCH_SUCCESS, searchResponse); } @@ -63,7 +64,7 @@ public CommonResponse> getPlaceInfoWithLocationAndKeyword( @GetMapping("/search/keyword") @Operation(summary = "키워드 기반 장소 검색") public CommonResponse> getPlaceInfoWithKeyword(@RequestParam String keyword) { - List searchResponse = placeService.searchByKeyword(keyword); + List searchResponse = placeService.searchByKeyword(keyword, 3); return CommonResponse.success(PlaceResponseCode.PLACE_SEARCH_SUCCESS, searchResponse); } @@ -130,7 +131,8 @@ public CommonResponse> getPartnershipPlaces( example = "5" ) @RequestParam(defaultValue = "5") int size) { - List response = partnershipPlaceService.getPartnershipPlaces(userId, cursor, size, lat, lng); + List response = partnershipPlaceService.getPartnershipPlaces(userId, cursor, size, lat, + lng); return CommonResponse.success(PlaceResponseCode.CHECK_PARTNERSHIP_PLACES_SUCCESS, response); } @@ -190,4 +192,16 @@ public CommonResponse getPartnershipPlaceDetail( PlaceResponseCode.CHECK_ONE_PARTNERSHIP_PLACE_SUCCESS, partnershipPlaceService.getPartnershipDetail(postId, userId, lat, lng)); } + + @GetMapping("/random") + @Operation(summary = "시간대별 랜덤 장소 추천 (제휴 장소 2, 랜덤 장소 2) (홈화면)") + public CommonResponse getRandomPlaceByTime( + @CurrentUserId Long userId, + @RequestParam double lat, + @RequestParam double lng + ) { + RecommendPlaceByTimeResponse response = placeService.findRecommendations(userId, lat, lng); + + return CommonResponse.success(PlaceResponseCode.GET_RANDOM_PLACE_SUCCESS, response); + } } diff --git a/src/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.java b/src/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.java index b6797192..b4a52f8f 100644 --- a/src/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.java +++ b/src/main/java/com/campus/campus/domain/place/presentation/PlaceResponseCode.java @@ -14,7 +14,8 @@ public enum PlaceResponseCode implements ResponseCodeInterface { PLACE_SEARCH_SUCCESS(200, HttpStatus.OK, "키워드 장소 검색이 성공적으로 완료되었습니다."), CHECK_PARTNERSHIP_PLACE_SUCCESS(200, HttpStatus.OK, "제휴 장소 조회가 완료되었습니다."), CHECK_PARTNERSHIP_PLACES_SUCCESS(200, HttpStatus.OK, "제휴 장소 리스트 조회가 완료되었습니다."), - CHECK_ONE_PARTNERSHIP_PLACE_SUCCESS(200, HttpStatus.OK, "제휴 장소 단건 조회가 완료되었습니다."); + CHECK_ONE_PARTNERSHIP_PLACE_SUCCESS(200, HttpStatus.OK, "제휴 장소 단건 조회가 완료되었습니다."), + GET_RANDOM_PLACE_SUCCESS(200, HttpStatus.OK, "시간대별 랜덤 장소 조회가 완료되었습니다."); private final int code; private final HttpStatus status; diff --git a/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java b/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java index deacd8e6..3fa74547 100644 --- a/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java +++ b/src/main/java/com/campus/campus/domain/user/application/service/KakaoOauthService.java @@ -131,4 +131,4 @@ private User findOrCreateUser(KakaoUserResponse kakaoUserResponse) { return userRepository.save(newUser); }); } -} \ No newline at end of file +}