diff --git a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java index 51248894..1efd1536 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java +++ b/src/main/java/com/campus/campus/domain/councilpost/application/service/StudentCouncilPostService.java @@ -70,7 +70,7 @@ public GetPostResponse create(Long councilId, PostRequest dto) { NormalizedDateTime normalized = dto.category().validateAndNormalize(dto); //Place 객체 생성 - Place place = placeService.findOrCreatePlace(dto); + Place place = placeService.findOrCreatePlace(dto.place()); StudentCouncilPost post = studentCouncilPostMapper.createStudentCouncilPost( writer, place, dto, normalized.startDateTime(), normalized.endDateTime() @@ -214,7 +214,7 @@ public GetPostResponse update(Long councilId, Long postId, PostRequest dto) { Place place = post.getPlace(); if (dto.place() != null && (place == null || !dto.place().placeName().equals(place.getPlaceName()))) { - place = placeService.findOrCreatePlace(dto); + place = placeService.findOrCreatePlace(dto.place()); } post.update( diff --git a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/PostImageRepository.java b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/PostImageRepository.java index 861cce6a..42724ce1 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/domain/repository/PostImageRepository.java +++ b/src/main/java/com/campus/campus/domain/councilpost/domain/repository/PostImageRepository.java @@ -24,4 +24,6 @@ public interface PostImageRepository extends JpaRepository { order by pi.id asc """) List findImageUrlsByPost(@Param("post") StudentCouncilPost post); + + List findAllByPostIn(List posts); } 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 2cd6397c..e1e662d3 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 @@ -220,6 +220,7 @@ Page findUpcomingMajorEvents( LEFT JOIN w.school s LEFT JOIN w.college c LEFT JOIN w.major m + JOIN p.place pl WHERE w.deletedAt IS NULL AND p.category = :category AND p.startDateTime <= :now @@ -249,7 +250,7 @@ List findByUserScopeWithCursor( SELECT p FROM StudentCouncilPost p JOIN p.writer w - JOIN p.place pl + JOIN FETCH p.place pl LEFT JOIN w.school s LEFT JOIN w.college c LEFT JOIN w.major m diff --git a/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java index 360ad381..8d83b1ec 100644 --- a/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java +++ b/src/main/java/com/campus/campus/domain/councilpost/presentation/StudentCouncilPostForUserController.java @@ -34,6 +34,7 @@ @RequiredArgsConstructor @Slf4j public class StudentCouncilPostForUserController { + private final StudentCouncilPostForUserService postService; @PostMapping("/{postId}/like") @Operation(summary = "학생회 게시글 좋아요 토글") @@ -56,8 +57,6 @@ public CommonResponse> getLikedPosts( return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, responses); } - private final StudentCouncilPostForUserService postService; - @GetMapping("/school") @Operation( summary = "학교 학생회 게시글 목록 조회", @@ -79,10 +78,11 @@ public CommonResponse> getSchoolPosts( @RequestParam(required = false) PostCategory category, @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "20") int size, - @RequestParam(required = false) Long excludePostId, + @RequestParam(required = false) Long excludePostId, @CurrentUserId Long userId ) { - Page responseDto = postService.findSchoolPosts(category, page, size, userId, excludePostId); + Page responseDto = postService.findSchoolPosts(category, page, size, userId, + excludePostId); return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, responseDto); } @@ -111,7 +111,8 @@ public CommonResponse> getCollegePosts( @RequestParam(required = false) Long excludePostId, @CurrentUserId Long userId ) { - Page responseDto = postService.findCollegePosts(category, page, size, userId, excludePostId); + Page responseDto = postService.findCollegePosts(category, page, size, userId, + excludePostId); return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, responseDto); } @@ -141,7 +142,8 @@ public CommonResponse> getMajorPosts( @RequestParam(required = false) Long excludePostId, @CurrentUserId Long userId ) { - Page responseDto = postService.findMajorPosts(category, page, size, userId, excludePostId); + Page responseDto = postService.findMajorPosts(category, page, size, userId, + excludePostId); return CommonResponse.success(StudentCouncilPostResponseCode.POST_LIST_READ_SUCCESS, responseDto); } diff --git a/src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipController.java b/src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipController.java deleted file mode 100644 index 126f4f5d..00000000 --- a/src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipController.java +++ /dev/null @@ -1,124 +0,0 @@ -package com.campus.campus.domain.partnership.presentation; - -import java.util.List; - -import org.springframework.web.bind.annotation.GetMapping; -import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; -import org.springframework.web.bind.annotation.RestController; - -import com.campus.campus.domain.partnership.application.dto.response.PartnershipPinResponse; -import com.campus.campus.domain.partnership.application.service.PartnershipService; -import com.campus.campus.domain.place.application.dto.response.partnership.PartnershipResponse; -import com.campus.campus.global.annotation.CurrentUserId; -import com.campus.campus.global.common.response.CommonResponse; - -import io.swagger.v3.oas.annotations.Operation; -import io.swagger.v3.oas.annotations.Parameter; -import io.swagger.v3.oas.annotations.media.ExampleObject; -import lombok.RequiredArgsConstructor; - -@RestController -@RequestMapping("/api/partnership") -@RequiredArgsConstructor -public class PartnershipController { - - private final PartnershipService partnershipService; - - @GetMapping("/list") - @Operation(summary = "리스트로 제휴 전체 조회", description = "무한 스크롤 방식으로 제휴 장소 목록을 조회합니다.") - public CommonResponse> getPartnershipPlaces( - @CurrentUserId Long userId, - @Parameter( - description = "현재 위치의 위도", - example = "37.50415" - ) - @RequestParam double lat, - @Parameter( - description = "현재 위치의 경도", - example = "126.9570" - ) - @RequestParam double lng, - @Parameter( - description = """ - 무한 스크롤 커서 값. - - 첫 요청 시 null - - 다음 요청부터는 이전 응답의 nextCursor 값 - """, - examples = { - @ExampleObject( - name = "첫 요청", - value = "null" - ), - @ExampleObject( - name = "다음 요청", - value = "120" - ) - } - ) - @RequestParam(required = false) Long cursor, - @Parameter( - description = "한 번에 조회할 개수", - example = "5" - ) - @RequestParam(defaultValue = "5") int size) { - List response = partnershipService.getPartnershipPlaces(userId, cursor, size, lat, lng); - return CommonResponse.success(PartnershipResponseCode.CHECK_PARTNERSHIP_PLACES_SUCCESS, response); - } - - @GetMapping("/map") - @Operation( - summary = "지도에서 제휴 장소 조회", - description = """ - 현재 지도 화면(bounds) 안에 있는 제휴 장소들을 조회합니다. - - bounds는 지도 화면의 남서/북동 좌표입니다. - - 지도 이동 또는 확대/축소 시 재호출됩니다. - """) - public CommonResponse> getPartnershipMap( - @CurrentUserId Long userId, - @Parameter( - description = "지도 화면의 남쪽(최소) 위도", - example = "37.497" - ) - @RequestParam Double minLat, - @Parameter( - description = "지도 화면의 북쪽(최대) 위도", - example = "37.512" - ) - @RequestParam Double maxLat, - @Parameter( - description = "지도 화면의 서쪽(최소) 경도", - example = "126.953" - ) - @RequestParam Double minLng, - @Parameter( - description = "지도 화면의 동쪽(최대) 경도", - example = "126.982" - ) - @RequestParam Double maxLng - ) { - return CommonResponse.success( - PartnershipResponseCode.CHECK_PARTNERSHIP_PLACES_SUCCESS, - partnershipService.findPartnerInBounds(userId, minLat, maxLat, minLng, maxLng) - ); - } - - @GetMapping("/detail") - @Operation(summary = "제휴 장소 상세 조회(맵에서 핀 클릭 시)") - public CommonResponse getPartnershipDetail( - @Parameter(description = "현재 위치의 위도", example = "37.50415") - @RequestParam double lat, - - @Parameter(description = "현재 위치의 경도", example = "126.9570") - @RequestParam double lng, - - @Parameter(description = "제휴글 ID", example = "10") - @RequestParam Long postId, - - @CurrentUserId Long userId - ) { - return CommonResponse.success( - PartnershipResponseCode.CHECK_ONE_PARTNERSHIP_PLACE_SUCCESS, - partnershipService.getPartnershipDetail(postId, userId, lat, lng)); - } -} diff --git a/src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipResponseCode.java b/src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipResponseCode.java deleted file mode 100644 index f8504161..00000000 --- a/src/main/java/com/campus/campus/domain/partnership/presentation/PartnershipResponseCode.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.campus.campus.domain.partnership.presentation; - -import org.springframework.http.HttpStatus; - -import com.campus.campus.global.common.response.ResponseCodeInterface; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -@Getter -@AllArgsConstructor -public enum PartnershipResponseCode implements ResponseCodeInterface { - - PLACE_SAVE_SUCCESS(200, HttpStatus.OK, "좋아요 처리가 완료되었습니다."), - CHECK_PARTNERSHIP_PLACES_SUCCESS(200, HttpStatus.OK, "제휴 장소 리스트 조회가 완료되었습니다."), - CHECK_ONE_PARTNERSHIP_PLACE_SUCCESS(200, HttpStatus.OK, "제휴 장소 단건 조회가 완료되었습니다."); - - private final int code; - private final HttpStatus status; - private final String message; -} diff --git a/src/main/java/com/campus/campus/domain/partnership/application/dto/response/PartnershipPinResponse.java b/src/main/java/com/campus/campus/domain/place/application/dto/response/PartnershipPinResponse.java similarity index 87% rename from src/main/java/com/campus/campus/domain/partnership/application/dto/response/PartnershipPinResponse.java rename to src/main/java/com/campus/campus/domain/place/application/dto/response/PartnershipPinResponse.java index 1dc74fe1..2c2119dd 100644 --- a/src/main/java/com/campus/campus/domain/partnership/application/dto/response/PartnershipPinResponse.java +++ b/src/main/java/com/campus/campus/domain/place/application/dto/response/PartnershipPinResponse.java @@ -1,4 +1,4 @@ -package com.campus.campus.domain.partnership.application.dto.response; +package com.campus.campus.domain.place.application.dto.response; import io.swagger.v3.oas.annotations.media.Schema; 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 6ff69c74..d4d6b494 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 @@ -6,10 +6,9 @@ import com.campus.campus.domain.council.domain.entity.CouncilType; import com.campus.campus.domain.councilpost.domain.entity.StudentCouncilPost; -import com.campus.campus.domain.partnership.application.dto.response.PartnershipPinResponse; +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.SavedPlaceInfo; -import com.campus.campus.domain.place.application.dto.response.geocoder.AddressResponse; import com.campus.campus.domain.place.application.dto.response.naver.NaverSearchResponse; import com.campus.campus.domain.place.application.dto.response.partnership.PartnershipResponse; import com.campus.campus.domain.place.domain.entity.Coordinate; diff --git a/src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java b/src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java similarity index 75% rename from src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java rename to src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java index 3311b68b..90563e35 100644 --- a/src/main/java/com/campus/campus/domain/partnership/application/service/PartnershipService.java +++ b/src/main/java/com/campus/campus/domain/place/application/service/PartnershipPlaceService.java @@ -1,9 +1,12 @@ -package com.campus.campus.domain.partnership.application.service; +package com.campus.campus.domain.place.application.service; import java.time.LocalDateTime; import java.util.AbstractMap; +import java.util.Collections; import java.util.List; import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; @@ -15,10 +18,11 @@ import com.campus.campus.domain.councilpost.application.exception.PlaceInfoNotFoundException; import com.campus.campus.domain.councilpost.application.exception.PostNotFoundException; import com.campus.campus.domain.councilpost.domain.entity.PostCategory; +import com.campus.campus.domain.councilpost.domain.entity.PostImage; import com.campus.campus.domain.councilpost.domain.entity.StudentCouncilPost; import com.campus.campus.domain.councilpost.domain.repository.PostImageRepository; import com.campus.campus.domain.councilpost.domain.repository.StudentCouncilPostRepository; -import com.campus.campus.domain.partnership.application.dto.response.PartnershipPinResponse; +import com.campus.campus.domain.place.application.dto.response.PartnershipPinResponse; import com.campus.campus.domain.place.application.dto.response.partnership.PartnershipResponse; import com.campus.campus.domain.place.application.mapper.PlaceMapper; import com.campus.campus.domain.place.domain.entity.Place; @@ -34,7 +38,7 @@ @Slf4j @RequiredArgsConstructor @Service -public class PartnershipService { +public class PartnershipPlaceService { private final UserRepository userRepository; private final LikedPlacesRepository likedPlacesRepository; @@ -42,7 +46,7 @@ public class PartnershipService { private final PlaceMapper placeMapper; private final StudentCouncilPostRepository studentCouncilPostRepository; - @Transactional + @Transactional(readOnly = true) public List getPartnershipPlaces(Long userId, Long cursor, int size, double userLat, double userLng) { User user = userRepository.findById(userId) @@ -58,45 +62,67 @@ public List getPartnershipPlaces(Long userId, Long cursor, //유저가 속한 학생회들의 제휴글(major/college/school) 전부 조회 List posts = studentCouncilPostRepository.findByUserScopeWithCursor( - majorId, - collegeId, - schoolId, + majorId, collegeId, schoolId, PostCategory.PARTNERSHIP, - CouncilType.MAJOR_COUNCIL, - CouncilType.COLLEGE_COUNCIL, - CouncilType.SCHOOL_COUNCIL, - cursor, - LocalDateTime.now(), - pageable + CouncilType.MAJOR_COUNCIL, CouncilType.COLLEGE_COUNCIL, CouncilType.SCHOOL_COUNCIL, + cursor, LocalDateTime.now(), pageable ); - return posts.stream() - .filter(post -> post.getPlace() != null && post.getPlace().getCoordinate() != null) + List> sortedEntries = posts.stream() .map(post -> { Place place = post.getPlace(); - double distanceMeter = GeoUtil.distanceMeter( userLat, userLng, place.getCoordinate().latitude(), place.getCoordinate().longitude() ); - - // post + distance를 함께 묶음 return new AbstractMap.SimpleEntry<>(post, distanceMeter); }) - .sorted(Map.Entry.comparingByValue()) // 거리순 정렬 + .sorted(Map.Entry.comparingByValue()) .limit(size) + .toList(); + + if (sortedEntries.isEmpty()) { + return List.of(); + } + + List targetPosts = sortedEntries.stream() + .map(AbstractMap.SimpleEntry::getKey) + .toList(); + + Set placeIds = targetPosts.stream() + .map(post -> post.getPlace().getPlaceId()) + .collect(Collectors.toSet()); + + Map> postImageMap = postImageRepository.findAllByPostIn(targetPosts) + .stream() + .collect(Collectors.groupingBy( + img -> img.getPost().getId(), + Collectors.mapping(PostImage::getImageUrl, Collectors.toList()) + )); + + Set likedPlaceIds; + if (placeIds.isEmpty()) { + likedPlaceIds = Collections.emptySet(); + } else { + likedPlaceIds = likedPlacesRepository.findLikedPlaceIds(userId, placeIds); + } + + return sortedEntries.stream() .map(entry -> { StudentCouncilPost post = entry.getKey(); double distanceMeter = entry.getValue(); double rounded = Math.round(distanceMeter * 100.0) / 100.0; + List images = postImageMap.getOrDefault(post.getId(), List.of()); + boolean isLiked = likedPlaceIds.contains(post.getPlace().getPlaceId()); + return placeMapper.toPartnershipResponse( user, post, post.getPlace(), - isLiked(post.getPlace(), user), - getImgUrls(post), + isLiked, + images, rounded ); }) @@ -144,9 +170,7 @@ public PartnershipResponse getPartnershipDetail(Long postId, Long userId, double .orElseThrow(UserNotFoundException::new); double distanceMeter = GeoUtil.distanceMeter( - userLat, userLng, - place.getCoordinate().latitude(), - place.getCoordinate().longitude() + userLat, userLng, place.getCoordinate().latitude(), place.getCoordinate().longitude() ); double rounded = Math.round(distanceMeter * 100.0) / 100.0; 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 ef421082..2c224b09 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 @@ -112,14 +112,23 @@ public List search(double lat, double lng, String keyword) { return futures.stream().map(CompletableFuture::join).toList(); } - @Transactional - public Place findOrCreatePlace(PostRequest request) { - SavedPlaceInfo place = request.place(); + public Place findOrCreatePlace(SavedPlaceInfo place) { String placeKey = place.placeKey(); - //이미 Place 존재하는지 확인 후 없으면 객체 생성 후 저장 return placeRepository.findByPlaceKey(placeKey) - .orElseGet(() -> placeRepository.save(placeMapper.createPlace(place))); + .orElseGet(() -> { + try { + Place newPlace = placeRepository.save(placeMapper.createPlace(place)); + + migrateImagesToOci(newPlace.getPlaceKey(), place.imgUrls()); + + return newPlace; + } catch (DataIntegrityViolationException e) { + log.info("해당 키에 대한 장소 동시 생성이 감지되었습니다.: {}", placeKey); + return placeRepository.findByPlaceKey(placeKey) + .orElseThrow(PlaceCreationException::new); + } + }); } //장소 저장 @@ -218,6 +227,9 @@ private String normalizeNaverMapLink(String link) { } private void migrateImagesToOci(String placeKey, List imageUrls) { + if (imageUrls == null || imageUrls.isEmpty()) { + return; + } //google 이미지 OCI 업로드 for (String googleUrl : imageUrls) { diff --git a/src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java b/src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java index 3a644c3c..90300741 100644 --- a/src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java +++ b/src/main/java/com/campus/campus/domain/place/domain/repository/LikedPlacesRepository.java @@ -1,6 +1,5 @@ package com.campus.campus.domain.place.domain.repository; -import java.util.List; import java.util.Optional; import java.util.Set; @@ -19,12 +18,12 @@ public interface LikedPlacesRepository extends JpaRepository { @Query(""" SELECT lp.place.placeId FROM LikedPlace lp - WHERE lp.user.id=:userId + WHERE lp.user.id=:userId and lp.place.placeId in :placeIds """) Set findLikedPlaceIds( @Param("userId") Long userId, - @Param("placeIds") List placeIds + @Param("placeIds") Set placeIds ); boolean existsByUserAndPlace(User user, Place place); 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 150cdbd7..e2453e51 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 @@ -6,14 +6,18 @@ import java.util.concurrent.TimeUnit; import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.client.reactive.ReactorClientHttpConnector; import org.springframework.stereotype.Component; +import org.springframework.web.reactive.function.client.ExchangeStrategies; import org.springframework.web.reactive.function.client.WebClient; import com.campus.campus.domain.place.application.dto.response.google.GooglePhoto; import com.campus.campus.domain.place.application.dto.response.google.GooglePlaceDetailResponse; import com.campus.campus.domain.place.application.dto.response.google.GoogleTextSearchResponse; +import io.netty.channel.ChannelOption; import lombok.extern.slf4j.Slf4j; +import reactor.netty.http.client.HttpClient; @Component @Slf4j @@ -28,9 +32,19 @@ public class GooglePlaceClient { public GooglePlaceClient( @Value("${map.google.places.api-key}") String apiKey ) { + HttpClient httpClient = HttpClient.create() + .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000) + .followRedirect(true); + + ExchangeStrategies strategies = ExchangeStrategies.builder() + .codecs(configurer -> configurer.defaultCodecs().maxInMemorySize(10 * 1024 * 1024)) + .build(); this.apiKey = apiKey; + this.webClient = WebClient.builder() .baseUrl(BASE_URL) + .clientConnector(new ReactorClientHttpConnector(httpClient)) + .exchangeStrategies(strategies) .build(); } @@ -148,7 +162,7 @@ private List getPhotoReferences(String placeId) { */ private String buildPhotoUrl(String photoRef) { return "https://maps.googleapis.com/maps/api/place/photo" - + "?maxWidth=800" + + "?maxwidth=800" + "&photo_reference=" + photoRef + "&key=" + apiKey; } 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 48605f69..b91de506 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 @@ -10,9 +10,12 @@ import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; +import com.campus.campus.domain.place.application.dto.response.PartnershipPinResponse; +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; import com.campus.campus.domain.place.application.dto.response.geocoder.AddressResponse; +import com.campus.campus.domain.place.application.dto.response.partnership.PartnershipResponse; import com.campus.campus.domain.place.application.service.PlaceService; import com.campus.campus.domain.place.infrastructure.geocoder.GeoCoderClient; import com.campus.campus.global.annotation.CurrentUserId; @@ -20,6 +23,7 @@ import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.Parameter; +import io.swagger.v3.oas.annotations.media.ExampleObject; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; @@ -29,6 +33,7 @@ public class PlaceController { private final PlaceService placeService; + private final PartnershipPlaceService partnershipPlaceService; private final GeoCoderClient geoCoderClient; @GetMapping("/search") @@ -78,6 +83,100 @@ public CommonResponse likePlace(@Valid @RequestBody SavedPlaceInfo return CommonResponse.success(PlaceResponseCode.PLACE_SAVE_SUCCESS, response); } - //가게 상세 조회 (리뷰 기능 구현 완료 후) + @GetMapping("/partnership") + @Operation(summary = "리스트로 제휴 장소 전체 조회", description = "무한 스크롤 방식으로 제휴 장소 목록을 조회합니다.") + public CommonResponse> getPartnershipPlaces( + @CurrentUserId Long userId, + @Parameter( + description = "현재 위치의 위도", + example = "37.50415" + ) + @RequestParam double lat, + @Parameter( + description = "현재 위치의 경도", + example = "126.9570" + ) + @RequestParam double lng, + @Parameter( + description = """ + 무한 스크롤 커서 값. + - 첫 요청 시 null + - 다음 요청부터는 이전 응답의 nextCursor 값 + """, + examples = { + @ExampleObject( + name = "첫 요청", + value = "null" + ), + @ExampleObject( + name = "다음 요청", + value = "120" + ) + } + ) + @RequestParam(required = false) Long cursor, + @Parameter( + description = "한 번에 조회할 개수", + example = "5" + ) + @RequestParam(defaultValue = "5") int size) { + List response = partnershipPlaceService.getPartnershipPlaces(userId, cursor, size, lat, lng); + return CommonResponse.success(PlaceResponseCode.CHECK_PARTNERSHIP_PLACES_SUCCESS, response); + } + @GetMapping("/partnership/map") + @Operation( + summary = "지도에서 제휴 장소 조회", + description = """ + 현재 지도 화면(bounds) 안에 있는 제휴 장소들을 조회합니다. + - bounds는 지도 화면의 남서/북동 좌표입니다. + - 지도 이동 또는 확대/축소 시 재호출됩니다. + """) + public CommonResponse> getPartnershipPlacesInMap( + @CurrentUserId Long userId, + @Parameter( + description = "지도 화면의 남쪽(최소) 위도", + example = "37.497" + ) + @RequestParam Double minLat, + @Parameter( + description = "지도 화면의 북쪽(최대) 위도", + example = "37.512" + ) + @RequestParam Double maxLat, + @Parameter( + description = "지도 화면의 서쪽(최소) 경도", + example = "126.953" + ) + @RequestParam Double minLng, + @Parameter( + description = "지도 화면의 동쪽(최대) 경도", + example = "126.982" + ) + @RequestParam Double maxLng + ) { + return CommonResponse.success( + PlaceResponseCode.CHECK_PARTNERSHIP_PLACES_SUCCESS, + partnershipPlaceService.findPartnerInBounds(userId, minLat, maxLat, minLng, maxLng) + ); + } + + @GetMapping("/partnership/detail") + @Operation(summary = "제휴 장소 상세 조회(맵에서 핀 클릭 시)") + public CommonResponse getPartnershipPlaceDetail( + @Parameter(description = "현재 위치의 위도", example = "37.50415") + @RequestParam double lat, + + @Parameter(description = "현재 위치의 경도", example = "126.9570") + @RequestParam double lng, + + @Parameter(description = "제휴글 ID", example = "10") + @RequestParam Long postId, + + @CurrentUserId Long userId + ) { + return CommonResponse.success( + PlaceResponseCode.CHECK_ONE_PARTNERSHIP_PLACE_SUCCESS, + partnershipPlaceService.getPartnershipDetail(postId, userId, lat, lng)); + } } 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 0b1a6920..b6797192 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 @@ -12,7 +12,9 @@ public enum PlaceResponseCode implements ResponseCodeInterface { PLACE_SAVE_SUCCESS(200, HttpStatus.OK, "좋아요 처리가 완료되었습니다."), PLACE_SEARCH_SUCCESS(200, HttpStatus.OK, "키워드 장소 검색이 성공적으로 완료되었습니다."), - CHECK_PARTNERSHIP_PLACE_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, "제휴 장소 단건 조회가 완료되었습니다."); private final int code; private final HttpStatus status;