From b645c854ae2afc7fd6a7b879771e6d4d741b35aa Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Sat, 30 May 2026 23:18:16 +0900 Subject: [PATCH 1/4] =?UTF-8?q?fix:=20=EB=93=B1=EC=82=B0=20=EA=B8=B0?= =?UTF-8?q?=EB=A1=9D=20=EC=82=B0=20=EB=AA=A9=EB=A1=9D=20=EC=8D=B8=EB=84=A4?= =?UTF-8?q?=EC=9D=BC=20=EC=82=AC=EC=A7=84=20=EC=9D=91=EB=8B=B5=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetUserHikingMountainRecordResponse.java | 19 +++++- .../repository/HikingRecordRepository.java | 65 +++++++++++++++---- .../UserHikingMountainRecordProjection.java | 4 +- ...tUserHikingMountainRecordResponseTest.java | 65 +++++++++++++++++++ 4 files changed, 139 insertions(+), 14 deletions(-) create mode 100644 src/test/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponseTest.java diff --git a/src/main/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponse.java b/src/main/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponse.java index 0db8c57b..eab392b5 100644 --- a/src/main/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponse.java +++ b/src/main/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponse.java @@ -3,11 +3,13 @@ import com.semosan.api.domain.hiking.repository.projection.UserHikingMountainRecordProjection; import java.time.LocalDate; +import java.util.ArrayList; +import java.util.List; public record GetUserHikingMountainRecordResponse( Long mountainId, String mountainName, - String imageUrl, + List imageUrls, Long hikingCount, LocalDate lastHikedAt ) { @@ -17,9 +19,22 @@ public static GetUserHikingMountainRecordResponse from(UserHikingMountainRecordP return new GetUserHikingMountainRecordResponse( projection.getMountainId(), projection.getMountainName(), - projection.getImageUrl(), + buildImageUrls(projection.getImageUrl1(), projection.getImageUrl2()), projection.getHikingCount(), projection.getLastHikedAt().toLocalDate() ); } + + private static List buildImageUrls(String imageUrl1, String imageUrl2) { + List imageUrls = new ArrayList<>(2); + addIfPresent(imageUrls, imageUrl1); + addIfPresent(imageUrls, imageUrl2); + return imageUrls; + } + + private static void addIfPresent(List imageUrls, String imageUrl) { + if (imageUrl != null && !imageUrl.isBlank()) { + imageUrls.add(imageUrl); + } + } } diff --git a/src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java b/src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java index 489049b4..8211d6b2 100644 --- a/src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java +++ b/src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java @@ -46,17 +46,60 @@ AND NOT EXISTS ( @Query( value = """ SELECT - hr.mountain_id AS mountainId, - m.name AS mountainName, - m.image_urls->>0 AS imageUrl, - COUNT(hr.id) AS hikingCount, - MAX(hr.created_at) AS lastHikedAt - FROM hiking_records hr - JOIN hiking_members hm ON hm.hiking_record_id = hr.id - JOIN mountains m ON m.id = hr.mountain_id - WHERE hm.user_id = :userId - GROUP BY hr.mountain_id, m.name, m.image_urls->>0 - ORDER BY MAX(hr.created_at) DESC, hr.mountain_id DESC + grouped.mountain_id AS mountainId, + grouped.mountain_name AS mountainName, + photos.image_url1 AS imageUrl1, + photos.image_url2 AS imageUrl2, + grouped.hiking_count AS hikingCount, + grouped.last_hiked_at AS lastHikedAt + FROM ( + SELECT + hr.mountain_id, + m.name AS mountain_name, + m.location AS mountain_location, + COUNT(hr.id) AS hiking_count, + MAX(hr.created_at) AS last_hiked_at + FROM hiking_records hr + JOIN hiking_members hm ON hm.hiking_record_id = hr.id + JOIN mountains m ON m.id = hr.mountain_id + WHERE hm.user_id = :userId + GROUP BY hr.mountain_id, m.name, m.location + ) grouped + LEFT JOIN LATERAL ( + SELECT + MAX(CASE WHEN ranked.rn = 1 THEN ranked.image_url END) AS image_url1, + MAX(CASE WHEN ranked.rn = 2 THEN ranked.image_url END) AS image_url2 + FROM ( + SELECT + tp.image_url, + ROW_NUMBER() OVER ( + ORDER BY + ST_Distance( + grouped.mountain_location, + ST_SetSRID(ST_MakePoint(tp.lng, tp.lat), 4326)::geography + ) ASC, + tp.captured_at DESC, + tp.id DESC + ) AS rn + FROM tracking_photos tp + JOIN tracking_sessions ts ON ts.id = tp.tracking_session_id + JOIN hiking_records hr2 ON hr2.tracking_session_id = ts.id + JOIN hiking_members hm2 ON hm2.hiking_record_id = hr2.id + WHERE hm2.user_id = :userId + AND hr2.mountain_id = grouped.mountain_id + AND grouped.mountain_location IS NOT NULL + AND NULLIF(tp.image_url, '') IS NOT NULL + ORDER BY + ST_Distance( + grouped.mountain_location, + ST_SetSRID(ST_MakePoint(tp.lng, tp.lat), 4326)::geography + ) ASC, + tp.captured_at DESC, + tp.id DESC + LIMIT 2 + ) ranked + ) photos ON true + ORDER BY grouped.last_hiked_at DESC, grouped.mountain_id DESC """, countQuery = """ SELECT COUNT(DISTINCT hr.mountain_id) diff --git a/src/main/java/com/semosan/api/domain/hiking/repository/projection/UserHikingMountainRecordProjection.java b/src/main/java/com/semosan/api/domain/hiking/repository/projection/UserHikingMountainRecordProjection.java index b973467c..dcd3126c 100644 --- a/src/main/java/com/semosan/api/domain/hiking/repository/projection/UserHikingMountainRecordProjection.java +++ b/src/main/java/com/semosan/api/domain/hiking/repository/projection/UserHikingMountainRecordProjection.java @@ -8,7 +8,9 @@ public interface UserHikingMountainRecordProjection { String getMountainName(); - String getImageUrl(); + String getImageUrl1(); + + String getImageUrl2(); Long getHikingCount(); diff --git a/src/test/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponseTest.java b/src/test/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponseTest.java new file mode 100644 index 00000000..4eb06286 --- /dev/null +++ b/src/test/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponseTest.java @@ -0,0 +1,65 @@ +package com.semosan.api.domain.hiking.dto.response; + +import com.semosan.api.domain.hiking.repository.projection.UserHikingMountainRecordProjection; +import org.junit.jupiter.api.Test; + +import java.time.LocalDate; +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +class GetUserHikingMountainRecordResponseTest { + + @Test + void fromMapsImageUrlsInOrder() { + GetUserHikingMountainRecordResponse response = GetUserHikingMountainRecordResponse.from( + projection("near-summit-1", "near-summit-2") + ); + + assertThat(response.imageUrls()).containsExactly("near-summit-1", "near-summit-2"); + assertThat(response.lastHikedAt()).isEqualTo(LocalDate.of(2026, 5, 28)); + } + + @Test + void fromFiltersBlankImageUrls() { + GetUserHikingMountainRecordResponse response = GetUserHikingMountainRecordResponse.from( + projection("", "near-summit-2") + ); + + assertThat(response.imageUrls()).containsExactly("near-summit-2"); + } + + private UserHikingMountainRecordProjection projection(String imageUrl1, String imageUrl2) { + return new UserHikingMountainRecordProjection() { + @Override + public Long getMountainId() { + return 1L; + } + + @Override + public String getMountainName() { + return "관악산"; + } + + @Override + public String getImageUrl1() { + return imageUrl1; + } + + @Override + public String getImageUrl2() { + return imageUrl2; + } + + @Override + public Long getHikingCount() { + return 3L; + } + + @Override + public LocalDateTime getLastHikedAt() { + return LocalDateTime.of(2026, 5, 28, 10, 0); + } + }; + } +} From 8e5e1019ed6dd2daa1ddd1be4577807a400f2d4f Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Sat, 30 May 2026 23:33:34 +0900 Subject: [PATCH 2/4] =?UTF-8?q?refactor:=20=EC=A0=95=EC=83=81=20=EC=9D=B8?= =?UTF-8?q?=EA=B7=BC=20=EC=82=AC=EC=A7=84=20=EC=A1=B0=ED=9A=8C=20=EC=A1=B0?= =?UTF-8?q?=EC=9D=B8=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/hiking/repository/HikingRecordRepository.java | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java b/src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java index 8211d6b2..66201eaa 100644 --- a/src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java +++ b/src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java @@ -83,10 +83,9 @@ LEFT JOIN LATERAL ( ) AS rn FROM tracking_photos tp JOIN tracking_sessions ts ON ts.id = tp.tracking_session_id - JOIN hiking_records hr2 ON hr2.tracking_session_id = ts.id - JOIN hiking_members hm2 ON hm2.hiking_record_id = hr2.id - WHERE hm2.user_id = :userId - AND hr2.mountain_id = grouped.mountain_id + WHERE ts.user_id = :userId + AND ts.mountain_id = grouped.mountain_id + AND ts.status = 'COMPLETED' AND grouped.mountain_location IS NOT NULL AND NULLIF(tp.image_url, '') IS NOT NULL ORDER BY From dcccedbdc675c688d1a892d6685b3bd54152b4c6 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Sat, 30 May 2026 23:33:56 +0900 Subject: [PATCH 3/4] =?UTF-8?q?test:=20=EB=93=B1=EC=82=B0=20=EC=82=B0=20?= =?UTF-8?q?=EB=AA=A9=EB=A1=9D=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EB=B9=88=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetUserHikingMountainRecordResponseTest.java | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/test/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponseTest.java b/src/test/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponseTest.java index 4eb06286..c247e739 100644 --- a/src/test/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponseTest.java +++ b/src/test/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponseTest.java @@ -29,6 +29,15 @@ void fromFiltersBlankImageUrls() { assertThat(response.imageUrls()).containsExactly("near-summit-2"); } + @Test + void fromReturnsEmptyImageUrlsWhenNoPhotos() { + GetUserHikingMountainRecordResponse response = GetUserHikingMountainRecordResponse.from( + projection(null, null) + ); + + assertThat(response.imageUrls()).isEmpty(); + } + private UserHikingMountainRecordProjection projection(String imageUrl1, String imageUrl2) { return new UserHikingMountainRecordProjection() { @Override From 3835ac301381ede9065f2c4c6933416e0aa5c393 Mon Sep 17 00:00:00 2001 From: pooreumjung Date: Sat, 30 May 2026 23:34:17 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20=EB=93=B1=EC=82=B0=20=EC=82=B0?= =?UTF-8?q?=20=EB=AA=A9=EB=A1=9D=20=EC=9D=B4=EB=AF=B8=EC=A7=80=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20=EC=83=9D=EC=84=B1=20=EB=8B=A8=EC=88=9C=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../GetUserHikingMountainRecordResponse.java | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/main/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponse.java b/src/main/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponse.java index eab392b5..ef0f84d5 100644 --- a/src/main/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponse.java +++ b/src/main/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponse.java @@ -3,8 +3,8 @@ import com.semosan.api.domain.hiking.repository.projection.UserHikingMountainRecordProjection; import java.time.LocalDate; -import java.util.ArrayList; import java.util.List; +import java.util.stream.Stream; public record GetUserHikingMountainRecordResponse( Long mountainId, @@ -26,15 +26,8 @@ public static GetUserHikingMountainRecordResponse from(UserHikingMountainRecordP } private static List buildImageUrls(String imageUrl1, String imageUrl2) { - List imageUrls = new ArrayList<>(2); - addIfPresent(imageUrls, imageUrl1); - addIfPresent(imageUrls, imageUrl2); - return imageUrls; - } - - private static void addIfPresent(List imageUrls, String imageUrl) { - if (imageUrl != null && !imageUrl.isBlank()) { - imageUrls.add(imageUrl); - } + return Stream.of(imageUrl1, imageUrl2) + .filter(imageUrl -> imageUrl != null && !imageUrl.isBlank()) + .toList(); } }