Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,12 +54,13 @@ public interface PostRepository {
void insertHeart(@Param("postId") Long postId, @Param("userId") Long userId);

void deleteHeart(@Param("postId") Long postId, @Param("userId") Long userId);

List<PostListResponse> findPostsBySpotId(@Param("spotId") Long spotId, @Param("pageable") Pageable pageable);

long countPostsBySpotId(@Param("spotId") Long spotId);

void incrementHeartCount(@Param("postId") Long postId);

void decrementHeartCount(@Param("postId") Long postId);

}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import com.shotmap.spot.repository.SpotRepository;
import com.shotmap.spot.util.HaversineDistance;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import smile.clustering.DBSCAN;
Expand All @@ -22,6 +23,7 @@ public class SpotClusteringService {
private static final double EPSILON = 0.1;
private static final int MIN_POINT = 3;


@Transactional
public void generateSpots() {
deleteAllSpots();
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
package com.shotmap.spot.application;

import com.shotmap.post.domain.Post;
import com.shotmap.post.repository.PostRepository;
import com.shotmap.spot.domain.Spot;
import com.shotmap.spot.repository.SpotRepository;
import com.shotmap.spot.util.HaversineDistance;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import smile.clustering.DBSCAN;

import java.util.*;
import java.util.stream.Collectors;

@Slf4j
@Service
@RequiredArgsConstructor
public class SpotClusteringV2Service {
private final SpotRepository spotRepository;
private final PostRepository postRepository;

private static final double EPSILON = 0.1;
private static final int MIN_POINT = 3;


@Transactional
public void generateSpots() {

List<Post> posts = getAllPost();
Map<Integer, List<Post>> clusters = performClustering(posts);
Set<Long> validSpotIds = new HashSet<>();

for (List<Post> cluster : clusters.values()) {
Map<Long, Integer> countMap = new HashMap<>();
for (Post post : cluster) {
if (post.getSpotId() != null) {
countMap.merge(post.getSpotId(), 1, Integer::sum);
}
}

List<Map.Entry<Long, Integer>> sorted = countMap.entrySet().stream()
.sorted((a, b) -> b.getValue() - a.getValue()).toList();

Long SpotIdToUse = sorted.isEmpty() ? null : sorted.get(0).getKey();


if (SpotIdToUse == null) {
Spot newSpot = createSpot(cluster);
SpotIdToUse = newSpot.getId();
}
validSpotIds.add(SpotIdToUse);
assignSpotToPost(cluster, SpotIdToUse);
}

List<Long> allSpotIdsInDB = spotRepository.findAllSpotIds();
List<Long> orphanSpotIds = allSpotIdsInDB.stream()
.filter(id -> !validSpotIds.contains(id))
.toList();

if (!orphanSpotIds.isEmpty()) {
spotRepository.deleteByIds(orphanSpotIds);
}
}

public List<Post> getAllPost() {
return postRepository.findAllPost();
}

public Map<Integer, List<Post>> performClustering(List<Post> posts) {

double[][] coords = posts.stream().map(p -> new double[]{p.getLatitude(), p.getLongitude()})
.toArray((double[][]::new));

DBSCAN<double[]> dbscan = DBSCAN.fit(coords, new HaversineDistance(), MIN_POINT, EPSILON);
int[] labels = dbscan.y;

Map<Integer, List<Post>> clusterMap = new HashMap<>();
for (int i = 0; i < labels.length; i++) {
if (labels[i] == Integer.MAX_VALUE) {
continue;
}
clusterMap.computeIfAbsent(labels[i], k -> new ArrayList<>()).add(posts.get(i));
}

return clusterMap;
}

public Spot createSpot(List<Post> cluster) {
double avgLatitude = cluster.stream().mapToDouble(Post::getLatitude).average().orElse(0);
double avgLongitude = cluster.stream().mapToDouble(Post::getLongitude).average().orElse(0);
Spot spot = Spot.builder().latitude(avgLatitude).longitude(avgLongitude).build();
spotRepository.insert(spot);
return spot;
}

public void assignSpotToPost(List<Post> cluster, Long spotId) {
for (Post post : cluster) {
if (post.getSpotId() == null || !post.getSpotId().equals(spotId)) {
post.setSpotId(spotId);
}

}
postRepository.batchUpdateSpotId(cluster);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
package com.shotmap.spot.application;

import com.shotmap.post.domain.Post;
import com.shotmap.post.repository.PostRepository;
import com.shotmap.spot.domain.Spot;
import com.shotmap.spot.repository.SpotRepository;
import com.shotmap.spot.util.HaversineDistance;
import com.shotmap.spot.vo.PostToUpdate;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import smile.clustering.DBSCAN;

import java.util.*;

@Service
@RequiredArgsConstructor
public class SpotClusteringV3Service {
private final SpotRepository spotRepository;
private final PostRepository postRepository;

private static final double EPSILON = 0.1;
private static final int MIN_POINT = 3;


@Transactional
public void generateSpots() {
spotRepository.createTempPostSpotTable();

deleteAllSpots();
List<Post> posts = getAllPost();
Map<Integer, List<Post>> clusters = new LinkedHashMap<>(performClustering(posts));
List<PostToUpdate> updates = new ArrayList<>();
List<Spot> spotList = new ArrayList<>();

for (List<Post> cluster : clusters.values()) {
spotList.add(createSpot(cluster));
}
spotRepository.insertSpotsBulk(spotList);

Iterator<List<Post>> cIt = clusters.values().iterator();
Iterator<Spot> sIt = spotList.iterator();
while (cIt.hasNext() && sIt.hasNext()) {
long spotId = sIt.next().getId();
for (Post p : cIt.next()) {
updates.add(new PostToUpdate(p.getId(), spotId));
}
}
spotRepository.insertTempPostSpot(updates);
spotRepository.updatePostSpotIdFromTemp();

}

public void deleteAllSpots() {
spotRepository.deleteAll();
}

public List<Post> getAllPost() {
return postRepository.findAllPost();
}

public Map<Integer, List<Post>> performClustering(List<Post> posts) {

double[][] coords = posts.stream().map(p -> new double[]{p.getLatitude(), p.getLongitude()})
.toArray((double[][]::new));

DBSCAN<double[]> dbscan = DBSCAN.fit(coords, new HaversineDistance(), MIN_POINT, EPSILON);
int[] labels = dbscan.y;

Map<Integer, List<Post>> clusterMap = new HashMap<>();
for (int i = 0; i < labels.length; i++) {
if (labels[i] == Integer.MAX_VALUE) {
continue;
}
clusterMap.computeIfAbsent(labels[i], k -> new ArrayList<>()).add(posts.get(i));
}

return clusterMap;
}

public Spot createSpot(List<Post> cluster) {
double avgLatitude = cluster.stream().mapToDouble(Post::getLatitude).average().orElse(0);
double avgLongitude = cluster.stream().mapToDouble(Post::getLongitude).average().orElse(0);
Spot spot = Spot.builder().latitude(avgLatitude).longitude(avgLongitude).build();
return spot;
}

public void assignSpotToPost(List<Post> cluster, Long spotId) {
for (Post post : cluster) {
post.setSpotId(spotId);
}
postRepository.batchUpdateSpotId(cluster);
}
}
18 changes: 13 additions & 5 deletions src/main/java/com/shotmap/spot/presentation/SpotController.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,31 +4,33 @@
import com.shotmap.global.response.PagedResponse;

import com.shotmap.post.response.PostListResponse;
import com.shotmap.spot.application.*;
import com.shotmap.spot.request.SpotSearchNearbyRequest;
import com.shotmap.spot.request.SpotRecommendationRequest;
import com.shotmap.spot.response.SpotDetailResponse;
import com.shotmap.spot.response.SpotListResponse;
import com.shotmap.spot.response.SpotRecommendationResponse;
import com.shotmap.spot.application.SpotRecommendationService;
import com.shotmap.spot.application.SpotService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.web.bind.annotation.*;
import lombok.extern.slf4j.Slf4j;

import java.util.ArrayList;
import java.util.List;



@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/spots")
public class SpotController {

private final SpotService spotService;
private final SpotRecommendationService spotRecommendationService;
private final SpotClusteringService clusteringService;

@GetMapping("/nearby")

public ApiResponse<PagedResponse<SpotListResponse>> getNearbySpots(SpotSearchNearbyRequest request, @PageableDefault(size = 10, sort="heart_count") Pageable pageable){
PagedResponse<SpotListResponse> response = spotService.findNearbySpots(request, pageable);
return new ApiResponse<>(response);
Expand All @@ -48,8 +50,14 @@ public ApiResponse<List<SpotRecommendationResponse>> spotRecommendation(@Request
}

@GetMapping("/{spotId}/posts")
public ApiResponse<PagedResponse<PostListResponse>> getPostsBySpotId(@PathVariable Long spotId, @PageableDefault(size=4, sort="post_create_at") Pageable pageable){
public ApiResponse<PagedResponse<PostListResponse>> getPostsBySpotId(@PathVariable Long spotId, @PageableDefault(size = 4, sort = "post_create_at") Pageable pageable) {
PagedResponse<PostListResponse> response = spotService.findPostsBySpotId(spotId, pageable);
return new ApiResponse<>(response);
}

@GetMapping("/clustering")
public ApiResponse<?> spotClustering() {
clusteringService.generateSpots();
return new ApiResponse<>(null);
}
}
20 changes: 20 additions & 0 deletions src/main/java/com/shotmap/spot/repository/SpotRepository.java
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,13 @@
import com.shotmap.spot.request.SpotSearchNearbyRequest;
import com.shotmap.spot.response.*;

import com.shotmap.spot.vo.PostToUpdate;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

import java.util.Collection;
import java.util.List;
import java.util.Set;

@Mapper
public interface SpotRepository {
Expand All @@ -31,4 +33,22 @@ public interface SpotRepository {
List<SpotImageResponse> selectSpotImageUrlBySpotIds(@Param("list") List<Long> spotIds);

List<SpotTagResponse> selectSpotTagBySpotIds(@Param("list") List<Long> spotIds);

//클러스터링 v2용

void deleteByIds(@Param("ids") List<Long> ids);

List<Long> findAllSpotIds();

//클러스터링 v3용

void createTempPostSpotTable();

void insertTempPostSpot(@Param("posts") List<PostToUpdate> posts);

void updatePostSpotIdFromTemp();

void insertSpotsBulk(List<Spot> spots);


}
13 changes: 13 additions & 0 deletions src/main/java/com/shotmap/spot/vo/PostToUpdate.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.shotmap.spot.vo;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PostToUpdate {
private Long postId;
private Long spotId;
}
Loading