Skip to content

Commit

Permalink
Merge pull request #627 from woowacourse-teams/develop-BE
Browse files Browse the repository at this point in the history
[BE] 2.1.0 배포
  • Loading branch information
kpeel5839 authored Nov 18, 2023
2 parents 8f2395a + ddbceb8 commit 6c43ed9
Show file tree
Hide file tree
Showing 27 changed files with 772 additions and 113 deletions.
3 changes: 3 additions & 0 deletions backend/src/docs/asciidoc/topic.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,6 @@ operation::topic-controller-test/update[snippets='http-request,http-response']
=== 토픽 이미지 수정

operation::topic-controller-test/update-image[snippets='http-request,http-response']

=== 토픽 클러스터링 조회
operation::topic-controller-test/get-clusters-of-pins[snippets='http-request,http-response']
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@ public class Coordinate {
private static final double LATITUDE_UPPER_BOUND = 43;
private static final double LONGITUDE_LOWER_BOUND = 124;
private static final double LONGITUDE_UPPER_BOUND = 132;
private static final double EARTH_RADIUS = 6371.0;
private static final int CONVERT_TO_METER = 1000;

/*
* 4326은 데이터베이스에서 사용하는 여러 SRID 값 중, 일반적인 GPS기반의 위/경도 좌표를 저장할 때 쓰이는 값입니다.
Expand All @@ -34,7 +36,6 @@ private Coordinate(Point point) {
this.coordinate = point;
}


public static Coordinate of(double latitude, double longitude) {
validateRange(latitude, longitude);

Expand Down Expand Up @@ -62,4 +63,13 @@ public double getLongitude() {
return coordinate.getX();
}

public double calculateDistance(Coordinate coordinate) {
return Math.acos(
Math.sin(Math.toRadians(coordinate.getLatitude())) * Math.sin(Math.toRadians(this.getLatitude()))
+ (Math.cos(Math.toRadians(coordinate.getLatitude())) * Math.cos(
Math.toRadians(this.getLatitude())) * Math.cos(
Math.toRadians(coordinate.getLongitude() - this.getLongitude())))
) * EARTH_RADIUS * CONVERT_TO_METER;
}

}
22 changes: 4 additions & 18 deletions backend/src/main/java/com/mapbefine/mapbefine/pin/domain/Pin.java
Original file line number Diff line number Diff line change
Expand Up @@ -108,24 +108,6 @@ public void decreaseTopicPinCount() {
topic.decreasePinCount();
}

public void copyToTopic(Member creator, Topic topic) {
Pin copiedPin = Pin.createPinAssociatedWithLocationAndTopicAndMember(
pinInfo.getName(),
pinInfo.getDescription(),
location,
topic,
creator
);

copyPinImages(copiedPin);
}

private void copyPinImages(Pin pin) {
for (PinImage pinImage : pinImages) {
PinImage.createPinImageAssociatedWithPin(pinImage.getImageUrl(), pin);
}
}

public void addPinImage(PinImage pinImage) {
pinImages.add(pinImage);
}
Expand All @@ -138,6 +120,10 @@ public double getLongitude() {
return location.getLongitude();
}

public String getName() {
return pinInfo.getName();
}

public String getDescription() {
return pinInfo.getDescription();
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.mapbefine.mapbefine.pin.domain;

import com.mapbefine.mapbefine.pin.infrastructure.PinBatchRepositoryCustom;
import java.util.List;
import org.springframework.data.jpa.repository.EntityGraph;
import org.springframework.data.jpa.repository.JpaRepository;
Expand All @@ -9,11 +10,14 @@
import org.springframework.stereotype.Repository;

@Repository
public interface PinRepository extends JpaRepository<Pin, Long> {
public interface PinRepository extends JpaRepository<Pin, Long>, PinBatchRepositoryCustom {

@EntityGraph(attributePaths = {"location", "topic", "creator", "pinImages"})
List<Pin> findAll();

@EntityGraph(attributePaths = {"location", "topic", "creator", "pinImages"})
List<Pin> findAllByIdIn(List<Long> pinIds);

@EntityGraph(attributePaths = {"location", "topic", "creator", "pinImages"})
List<Pin> findAllByTopicId(Long topicId);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package com.mapbefine.mapbefine.pin.infrastructure;

import com.mapbefine.mapbefine.pin.domain.Pin;
import com.mapbefine.mapbefine.topic.domain.Topic;
import java.util.List;

public interface PinBatchRepositoryCustom {

int[] saveAllToTopic(Topic topicForCopy, List<Pin> originalPins);

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
package com.mapbefine.mapbefine.pin.infrastructure;

import static java.sql.Statement.EXECUTE_FAILED;

import com.mapbefine.mapbefine.pin.domain.Pin;
import com.mapbefine.mapbefine.pin.domain.PinImage;
import com.mapbefine.mapbefine.topic.domain.Topic;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.sql.Timestamp;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.IntStream;
import lombok.extern.slf4j.Slf4j;
import org.springframework.jdbc.core.BatchPreparedStatementSetter;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

@Slf4j
@Repository
public class PinBatchRepositoryCustomImpl implements PinBatchRepositoryCustom {

private final JdbcTemplate jdbcTemplate;

public PinBatchRepositoryCustomImpl(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}

public int[] saveAllToTopic(Topic topicForCopy, List<Pin> originalPins) {
int[] rowCount = bulkInsertPins(topicForCopy, originalPins);
List<PinImageInsertDto> pinImageInsertDtos = createPinImageInsertDtos(originalPins, rowCount);

if (pinImageInsertDtos.isEmpty()) {
return rowCount;
}
return bulkInsertPinImages(pinImageInsertDtos);
}

private int[] bulkInsertPins(Topic topicForCopy, List<Pin> originalPins) {
String bulkInsertSql = "INSERT INTO pin "
+ "(name, description, member_id, topic_id, location_id, "
+ "created_at, updated_at) "
+ "VALUES "
+ "(?, ?, ?, ?, ?, "
+ "?, ?)";
LocalDateTime createdAt = topicForCopy.getLastPinUpdatedAt();
Long topicId = topicForCopy.getId();
Long creatorId = topicForCopy.getCreator().getId();
log.debug("[Query] bulk insert size {} : {}", originalPins.size(), bulkInsertSql);

return jdbcTemplate.batchUpdate(bulkInsertSql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
Pin pin = originalPins.get(i);
ps.setString(1, pin.getName());
ps.setString(2, pin.getDescription());
ps.setLong(3, creatorId);
ps.setLong(4, topicId);
ps.setLong(5, pin.getLocation().getId());
ps.setTimestamp(6, Timestamp.valueOf(createdAt));
ps.setTimestamp(7, Timestamp.valueOf(createdAt));
log.trace("[Parameter Binding] {} : "
+ "name={}, description={}, member_id={}, topic_id={}, location_id={}, "
+ "created_at={}, updated_at={}",
i, pin.getName(), pin.getDescription(), creatorId, topicId, pin.getLocation().getId(),
createdAt, createdAt);
}

@Override
public int getBatchSize() {
return originalPins.size();
}
});
}

private List<PinImageInsertDto> createPinImageInsertDtos(List<Pin> originalPins, int[] rowCount) {
Long firstIdFromBatch = jdbcTemplate.queryForObject("SELECT last_insert_id()", Long.class);
validateId(firstIdFromBatch);

return IntStream.range(0, originalPins.size())
.filter(index -> rowCount[index] != EXECUTE_FAILED)
.mapToObj(index -> {
Pin pin = originalPins.get(index);
return PinImageInsertDto.of(pin.getPinImages(), firstIdFromBatch + index);
}).flatMap(Collection::stream)
.toList();
}

private void validateId(Long firstIdFromBatch) {
if (Objects.isNull(firstIdFromBatch)) {
throw new IllegalStateException("fail to batch update pins");
}
}

private int[] bulkInsertPinImages(List<PinImageInsertDto> pinImages) {
String bulkInsertSql = "INSERT INTO pin_image "
+ "(image_url, pin_id) "
+ "VALUES "
+ "(?, ?)";
log.debug("[Query] bulk insert size {} : {}", pinImages.size(), bulkInsertSql);

return jdbcTemplate.batchUpdate(bulkInsertSql, new BatchPreparedStatementSetter() {
@Override
public void setValues(PreparedStatement ps, int i) throws SQLException {
PinImageInsertDto pinImage = pinImages.get(i);
ps.setString(1, pinImage.imageUrl);
ps.setLong(2, pinImage.pinId);
log.trace("[Parameter Binding] {} : imageUrl={}, pinImage={} ",
i, pinImage.imageUrl, pinImage.pinId);
}

@Override
public int getBatchSize() {
return pinImages.size();
}
});
}

private record PinImageInsertDto(
String imageUrl,
Long pinId,
boolean isDeleted
) {

public static PinImageInsertDto of(PinImage pinImage, Long pinId) {
return new PinImageInsertDto(
pinImage.getImageUrl(),
pinId,
pinImage.isDeleted()
);
}

private static List<PinImageInsertDto> of(List<PinImage> pinImages, Long pinId) {
return pinImages.stream()
.map(pinImage -> PinImageInsertDto.of(pinImage, pinId))
.toList();
}
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,11 @@ public Long saveTopic(AuthMember member, TopicCreateRequest request) {
Topic topic = convertToTopic(member, request);
List<Long> pinIds = request.pins();

topicRepository.save(topic);
if (!pinIds.isEmpty()) {
copyPinsToTopic(member, topic, pinIds);
}

topicRepository.save(topic);

return topic.getId();
}

Expand Down Expand Up @@ -105,14 +104,14 @@ private void copyPinsToTopic(
) {
List<Pin> originalPins = findAllPins(pinIds);
validateCopyablePins(member, originalPins);
topic.increasePinCount(pinIds.size());
pinRepository.flush();

Member creator = findCreatorByAuthMember(member);

originalPins.forEach(pin -> pin.copyToTopic(creator, topic));
pinRepository.saveAllToTopic(topic, originalPins);
}

private List<Pin> findAllPins(List<Long> pinIds) {
List<Pin> findPins = pinRepository.findAllById(pinIds);
List<Pin> findPins = pinRepository.findAllByIdIn(pinIds);

if (pinIds.size() != findPins.size()) {
throw new PinBadRequestException(ILLEGAL_PIN_ID);
Expand All @@ -134,15 +133,13 @@ private void validateCopyablePins(AuthMember member, List<Pin> originalPins) {
public Long merge(AuthMember member, TopicMergeRequest request) {
Topic topic = convertToTopic(member, request);
List<Topic> originalTopics = findAllTopics(request.topics());

validateCopyableTopics(member, originalTopics);

Member creator = findCreatorByAuthMember(member);
List<Pin> originalPins = getAllPinsFromTopics(originalTopics);
originalPins.forEach(pin -> pin.copyToTopic(creator, topic));

topicRepository.save(topic);
topic.increasePinCount(originalPins.size());

topicRepository.save(topic);
pinRepository.saveAllToTopic(topic, originalPins);
return topic.getId();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,11 @@
import com.mapbefine.mapbefine.bookmark.domain.BookmarkRepository;
import com.mapbefine.mapbefine.member.domain.Member;
import com.mapbefine.mapbefine.member.domain.MemberRepository;
import com.mapbefine.mapbefine.pin.domain.Pin;
import com.mapbefine.mapbefine.topic.domain.Clusters;
import com.mapbefine.mapbefine.topic.domain.Topic;
import com.mapbefine.mapbefine.topic.domain.TopicRepository;
import com.mapbefine.mapbefine.topic.dto.response.ClusterResponse;
import com.mapbefine.mapbefine.topic.dto.response.TopicDetailResponse;
import com.mapbefine.mapbefine.topic.dto.response.TopicResponse;
import com.mapbefine.mapbefine.topic.exception.TopicException.TopicForbiddenException;
Expand Down Expand Up @@ -241,4 +244,24 @@ private List<TopicResponse> getUserBestTopicResponse(AuthMember authMember) {
)).toList();
}

public List<ClusterResponse> findClustersPinsByIds(
AuthMember authMember,
List<Long> topicIds,
Double imageDiameter
) {
List<Topic> topics = topicRepository.findByIdIn(topicIds);
topics.forEach(topic -> validateReadableTopic(authMember, topic));

List<Pin> allPins = topics.stream()
.map(Topic::getPins)
.flatMap(List::stream)
.toList();

return Clusters.from(allPins, imageDiameter)
.getClusters()
.stream()
.map(ClusterResponse::from)
.toList();
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.mapbefine.mapbefine.topic.domain;

import com.mapbefine.mapbefine.pin.domain.Pin;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import lombok.Getter;

@Getter
public class Cluster {

private final double latitude;
private final double longitude;
private final List<Pin> pins;

private Cluster(double latitude, double longitude, List<Pin> pins) {
this.latitude = latitude;
this.longitude = longitude;
this.pins = pins;
}

public static Cluster from(Pin representPin, List<Pin> pins) {
return new Cluster(representPin.getLatitude(), representPin.getLongitude(), rearrangePins(representPin, pins));
}

private static List<Pin> rearrangePins(Pin representPin, List<Pin> pins) {
List<Pin> arrangePins = new ArrayList<>(List.of(representPin));

pins.stream()
.filter(pin -> !Objects.equals(representPin, pin))
.forEach(arrangePins::add);

return arrangePins;
}

}
Loading

0 comments on commit 6c43ed9

Please sign in to comment.