diff --git a/docker-compose.yml b/docker-compose.yml index f4a0f4a..6b01c78 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -15,7 +15,15 @@ services: volumes: - ./db/schema.sql:/docker-entrypoint-initdb.d/01-schema.sql - ./db/data-attractions.sql:/docker-entrypoint-initdb.d/02-data-attractions.sql - command: --default-authentication-plugin=mysql_native_password + - ./db/dummy-data-insert.sql:/docker-entrypoint-initdb.d/03-dummy-data.sql + command: + - --default-authentication-plugin=mysql_native_password + - --max_connections=500 + - --innodb_buffer_pool_size=512M + - --innodb_log_file_size=128M + + + redis: image: redis:7.2 diff --git a/src/main/java/com/shotmap/global/config/SecurityConfig.java b/src/main/java/com/shotmap/global/config/SecurityConfig.java index 4d0d92d..23f0280 100644 --- a/src/main/java/com/shotmap/global/config/SecurityConfig.java +++ b/src/main/java/com/shotmap/global/config/SecurityConfig.java @@ -42,6 +42,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { .requestMatchers("/api/v1/attractions/**").permitAll() .requestMatchers("/api/v1/spots/**").permitAll() .requestMatchers("/api/v1/posts/**").permitAll() + .requestMatchers("/api/v2/posts/**").permitAll() .requestMatchers("/swagger-ui/**", "/v3/api-docs/**", "/swagger-ui.html", "/swagger-resources/**", "/webjars/**").permitAll() .requestMatchers(HttpMethod.OPTIONS, "/**").permitAll() // CORS Preflight 허용 .anyRequest().authenticated() diff --git a/src/main/java/com/shotmap/post/application/PostService.java b/src/main/java/com/shotmap/post/application/PostService.java index c5adbdd..6f1a2ca 100644 --- a/src/main/java/com/shotmap/post/application/PostService.java +++ b/src/main/java/com/shotmap/post/application/PostService.java @@ -199,4 +199,4 @@ public void cancelLikePost(Long postId, Long userId) { postRepository.deleteHeart(postId, userId); log.debug("좋아요 취소 완료 - postId={}, userId={}", postId, userId); } -} +} \ No newline at end of file diff --git a/src/main/java/com/shotmap/post/application/PostV2Service.java b/src/main/java/com/shotmap/post/application/PostV2Service.java new file mode 100644 index 0000000..dd33eed --- /dev/null +++ b/src/main/java/com/shotmap/post/application/PostV2Service.java @@ -0,0 +1,95 @@ +package com.shotmap.post.application; + +import com.shotmap.attraction.vo.Location; +import com.shotmap.global.request.PageRequest; +import com.shotmap.global.response.PagedResponse; +import com.shotmap.post.repository.PostV2Repository; +import com.shotmap.post.request.PostSearchNearbyRequest; +import com.shotmap.post.response.PostImageResponse; +import com.shotmap.post.response.PostListResponse; +import com.shotmap.post.response.PostTagResponse; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageImpl; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.math.BigDecimal; +import java.math.RoundingMode; +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +@Slf4j +@Service +@RequiredArgsConstructor +@Transactional +public class PostV2Service { + + private final PostV2Repository postRepository; + + + @Transactional(readOnly = true) + public PagedResponse findNearbyPosts(PostSearchNearbyRequest request, Pageable pageable){ + + setNeLocationAndSwLocation(request); + + PageRequest pageRequest = PageRequest.builder() + .data(request) + .pageable(pageable) + .build(); + + // 게시물 기본 정보 + 사용자 + 좋아요 수 + List posts = postRepository.findNearbyPosts(pageRequest); + + //게시물 id 추출 + List postIds = posts.stream() + .map(PostListResponse::getId).toList(); + + if (postIds.isEmpty()) { + return PagedResponse.of(new PageImpl<>(posts, pageable, 0)); + } + + // 게시물 이미지 조회 + Map> postImages = postRepository.selectPostImageUrlByPostIds(postIds) + .stream().collect(Collectors.groupingBy(PostImageResponse::postId)); + + //게시물 태그 조회 + Map> postTags = postRepository.selectPostTagByPostIds(postIds) + .stream().collect(Collectors.groupingBy(PostTagResponse::postId)); + + posts.forEach(post -> { + post.setImages(postImages.getOrDefault(post.getId(),List.of())); + post.setTags(postTags.getOrDefault(post.getId(),List.of())); + }); + + long totalItems = postRepository.countNearbyPosts(request); + Page page = new PageImpl<>(posts, pageable, totalItems); + return PagedResponse.of(page); + + } + + @Transactional(readOnly = true) + public void setNeLocationAndSwLocation(PostSearchNearbyRequest request){ + Location neLocation = request.getNeLocation(); //북동쪽 좌표 + Location swLocation = request.getSwLocation(); //남서쪽 좌표 + Location centerLocation = request.getCenterLocation(); + + BigDecimal latitudeGap = neLocation.getLatitude().subtract(swLocation.getLatitude()); + BigDecimal longitudeGap = neLocation.getLongitude().subtract(swLocation.getLongitude()); + + BigDecimal latitudeHalfGap = latitudeGap.divide(BigDecimal.valueOf(2), 6, RoundingMode.HALF_UP); + BigDecimal longitudeHalfGap = longitudeGap.divide(BigDecimal.valueOf(2),6,RoundingMode.HALF_UP); + + neLocation.setLatitude(centerLocation.getLatitude().add(latitudeHalfGap)); + neLocation.setLongitude(centerLocation.getLongitude().add(longitudeHalfGap)); + + swLocation.setLongitude(centerLocation.getLongitude().subtract(longitudeHalfGap)); + swLocation.setLatitude(centerLocation.getLatitude().subtract(latitudeHalfGap)); + + request.setNeLocation(neLocation); + request.setSwLocation(swLocation); + } +} diff --git a/src/main/java/com/shotmap/post/presentation/PostV2Controller.java b/src/main/java/com/shotmap/post/presentation/PostV2Controller.java new file mode 100644 index 0000000..41a5104 --- /dev/null +++ b/src/main/java/com/shotmap/post/presentation/PostV2Controller.java @@ -0,0 +1,28 @@ +package com.shotmap.post.presentation; + +import com.shotmap.global.response.ApiResponse; +import com.shotmap.global.response.PagedResponse; +import com.shotmap.post.application.PostV2Service; +import com.shotmap.post.request.PostSearchNearbyRequest; +import com.shotmap.post.response.PostListResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.web.PageableDefault; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@RequiredArgsConstructor +@RequestMapping("/api/v2/posts") +public class PostV2Controller { + + private final PostV2Service postService; + + @GetMapping("/nearby") + public ApiResponse> getNearbyPosts(PostSearchNearbyRequest request, @PageableDefault(size = 10, sort="created_at") Pageable pageable){ + + PagedResponse response = postService.findNearbyPosts(request, pageable); + return new ApiResponse<>(response); + } +} diff --git a/src/main/java/com/shotmap/post/repository/PostRepository.java b/src/main/java/com/shotmap/post/repository/PostRepository.java index 6a6daa9..c8b2b82 100644 --- a/src/main/java/com/shotmap/post/repository/PostRepository.java +++ b/src/main/java/com/shotmap/post/repository/PostRepository.java @@ -54,8 +54,8 @@ public interface PostRepository { void insertHeart(@Param("postId") Long postId, @Param("userId") Long userId); void deleteHeart(@Param("postId") Long postId, @Param("userId") Long userId); - + List findPostsBySpotId(@Param("spotId") Long spotId, @Param("pageable") Pageable pageable); long countPostsBySpotId(@Param("spotId") Long spotId); -} +} \ No newline at end of file diff --git a/src/main/java/com/shotmap/post/repository/PostV2Repository.java b/src/main/java/com/shotmap/post/repository/PostV2Repository.java new file mode 100644 index 0000000..0dc359e --- /dev/null +++ b/src/main/java/com/shotmap/post/repository/PostV2Repository.java @@ -0,0 +1,23 @@ +package com.shotmap.post.repository; + +import com.shotmap.global.request.PageRequest; +import com.shotmap.post.request.PostSearchNearbyRequest; +import com.shotmap.post.response.PostImageResponse; +import com.shotmap.post.response.PostListResponse; +import com.shotmap.post.response.PostTagResponse; +import org.apache.ibatis.annotations.Mapper; + +import java.util.List; + +@Mapper +public interface PostV2Repository { + + List findNearbyPosts(PageRequest pageRequest); + + List selectPostImageUrlByPostIds(List postIds); + + List selectPostTagByPostIds(List postIds); + + long countNearbyPosts(PostSearchNearbyRequest request); + +} diff --git a/src/main/java/com/shotmap/post/response/PostImageResponse.java b/src/main/java/com/shotmap/post/response/PostImageResponse.java index bb2f594..0f9d4c7 100644 --- a/src/main/java/com/shotmap/post/response/PostImageResponse.java +++ b/src/main/java/com/shotmap/post/response/PostImageResponse.java @@ -3,10 +3,11 @@ import com.shotmap.post.domain.PostImage; public record PostImageResponse( - Long id, + Long postId, + Long imageId, String imageUrl ) { - public PostImageResponse(PostImage image) { - this(image.getId(), image.getImageUrl()); + public PostImageResponse(PostImage postImage) { + this(postImage.getPostId(), postImage.getId(), postImage.getImageUrl()); } } diff --git a/src/main/java/com/shotmap/post/response/PostListResponse.java b/src/main/java/com/shotmap/post/response/PostListResponse.java index b0d06a6..35286b8 100644 --- a/src/main/java/com/shotmap/post/response/PostListResponse.java +++ b/src/main/java/com/shotmap/post/response/PostListResponse.java @@ -5,6 +5,7 @@ import com.shotmap.user.domain.User; import lombok.Getter; import lombok.NoArgsConstructor; +import lombok.Setter; import lombok.experimental.SuperBuilder; import java.time.LocalDateTime; @@ -23,8 +24,10 @@ public class PostListResponse { Long heartCount; //게시물 좋아요 수 LocalDateTime createdAt; //게시물 작성 시간 - List images; //게시물 사진 리스트 - List tags; //게시물 태그 리스틑 User user; //게시물 작성자 정보 + @Setter + List images; //게시물 사진 리스트 + @Setter + List tags; //게시물 태그 리스틑 } diff --git a/src/main/java/com/shotmap/post/response/PostTagResponse.java b/src/main/java/com/shotmap/post/response/PostTagResponse.java new file mode 100644 index 0000000..7d7baa2 --- /dev/null +++ b/src/main/java/com/shotmap/post/response/PostTagResponse.java @@ -0,0 +1,8 @@ +package com.shotmap.post.response; + +public record PostTagResponse( + Long postId, + Long tagId, + String name) +{ +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index dedb85f..73e19eb 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -8,6 +8,15 @@ spring: password: ${DB_PASSWORD} driver-class-name: com.mysql.cj.jdbc.Driver + hikari: + maximum-pool-size: 300 # 커넥션 풀의 최대 크기 + minimum-idle: 10 # 최소 유휴 커넥션 수 + idle-timeout: 600000 # 커넥션 유휴 타임아웃(ms) (기본: 600000ms = 10분) + max-lifetime: 1800000 # 커넥션 최대 생존 시간(ms) (기본: 30분) + connection-timeout: 30000 # 커넥션 요청 대기 최대 시간(ms) + leak-detection-threshold: 30000 # 커넥션 누수 감지 시간(ms) + + ai: openai: @@ -35,7 +44,7 @@ mybatis: type-aliases-package: com.shotmap configuration: map-underscore-to-camel-case: true - log-impl: org.apache.ibatis.logging.stdout.StdOutImpl + #log-impl: org.apache.ibatis.logging.stdout.StdOutImpl kakao: api: diff --git a/src/main/resources/mappers/PostMapper.xml b/src/main/resources/mappers/PostMapper.xml index e6540e4..dfb93b3 100644 --- a/src/main/resources/mappers/PostMapper.xml +++ b/src/main/resources/mappers/PostMapper.xml @@ -18,16 +18,16 @@ u.no AS user_no, u.nickname AS user_nickname, u.profile_image_url AS user_profile FROM post p LEFT JOIN ( - SELECT post_no, COUNT(*) AS heart_count - FROM heart - WHERE is_deleted = FALSE - GROUP BY post_no + SELECT post_no, COUNT(*) AS heart_count + FROM heart + WHERE is_deleted = FALSE + GROUP BY post_no )hc ON p.no = hc.post_no LEFT JOIN users u ON p.user_no = u.no WHERE p.latitude BETWEEN #{data.swLocation.latitude} AND #{data.neLocation.latitude} - AND p.longitude BETWEEN #{data.swLocation.longitude} AND #{data.neLocation.longitude} - AND p.is_deleted = false - AND u.is_deleted = false + AND p.longitude BETWEEN #{data.swLocation.longitude} AND #{data.neLocation.longitude} + AND p.is_deleted = false + AND u.is_deleted = false ORDER BY @@ -53,15 +53,15 @@ u.no AS user_no, u.nickname AS user_nickname, u.profile_image_url AS user_profile FROM post p LEFT JOIN ( - SELECT post_no, COUNT(*) AS heart_count - FROM heart - WHERE is_deleted = FALSE - GROUP BY post_no - )hc ON p.no = hc.post_no + SELECT post_no, COUNT(*) AS heart_count + FROM heart + WHERE is_deleted = FALSE + GROUP BY post_no + )hc ON p.no = hc.post_no JOIN users u ON p.user_no = u.no WHERE p.user_no = #{userId} - AND p.is_deleted = false - AND u.is_deleted = false + AND p.is_deleted = false + AND u.is_deleted = false ORDER BY p.created_at DESC LIMIT #{pageable.pageSize} OFFSET #{pageable.offset} @@ -75,17 +75,17 @@ - SELECT pi.no AS id, pi.image_url AS imageUrl, pi.post_no AS postId + - + SELECT pt.post_no AS postId, t.no AS tagId, t.name AS name FROM post_tag pt JOIN tag t ON pt.tag_no = t.no @@ -251,11 +251,11 @@ @@ -270,6 +270,6 @@ UPDATE heart SET is_deleted = TRUE WHERE post_no = #{postId} - AND user_no = #{userId} + AND user_no = #{userId} \ No newline at end of file diff --git a/src/main/resources/mappers/PostV2Mapper.xml b/src/main/resources/mappers/PostV2Mapper.xml new file mode 100644 index 0000000..745f944 --- /dev/null +++ b/src/main/resources/mappers/PostV2Mapper.xml @@ -0,0 +1,86 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file