Skip to content

Commit 9720874

Browse files
authored
Merge pull request #166 from SEMOSAN/feat/#165-hiking-record-summit-photos
[Feat] 산별 등산 기록 목록 API - 정상 인근 트래킹 사진 2장 응답 추가
2 parents 1c581b4 + 3835ac3 commit 9720874

4 files changed

Lines changed: 140 additions & 14 deletions

File tree

src/main/java/com/semosan/api/domain/hiking/dto/response/GetUserHikingMountainRecordResponse.java

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,13 @@
33
import com.semosan.api.domain.hiking.repository.projection.UserHikingMountainRecordProjection;
44

55
import java.time.LocalDate;
6+
import java.util.List;
7+
import java.util.stream.Stream;
68

79
public record GetUserHikingMountainRecordResponse(
810
Long mountainId,
911
String mountainName,
10-
String imageUrl,
12+
List<String> imageUrls,
1113
Long hikingCount,
1214
LocalDate lastHikedAt
1315
) {
@@ -17,9 +19,15 @@ public static GetUserHikingMountainRecordResponse from(UserHikingMountainRecordP
1719
return new GetUserHikingMountainRecordResponse(
1820
projection.getMountainId(),
1921
projection.getMountainName(),
20-
projection.getImageUrl(),
22+
buildImageUrls(projection.getImageUrl1(), projection.getImageUrl2()),
2123
projection.getHikingCount(),
2224
projection.getLastHikedAt().toLocalDate()
2325
);
2426
}
27+
28+
private static List<String> buildImageUrls(String imageUrl1, String imageUrl2) {
29+
return Stream.of(imageUrl1, imageUrl2)
30+
.filter(imageUrl -> imageUrl != null && !imageUrl.isBlank())
31+
.toList();
32+
}
2533
}

src/main/java/com/semosan/api/domain/hiking/repository/HikingRecordRepository.java

Lines changed: 53 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -46,17 +46,59 @@ AND NOT EXISTS (
4646
@Query(
4747
value = """
4848
SELECT
49-
hr.mountain_id AS mountainId,
50-
m.name AS mountainName,
51-
m.image_urls->>0 AS imageUrl,
52-
COUNT(hr.id) AS hikingCount,
53-
MAX(hr.created_at) AS lastHikedAt
54-
FROM hiking_records hr
55-
JOIN hiking_members hm ON hm.hiking_record_id = hr.id
56-
JOIN mountains m ON m.id = hr.mountain_id
57-
WHERE hm.user_id = :userId
58-
GROUP BY hr.mountain_id, m.name, m.image_urls->>0
59-
ORDER BY MAX(hr.created_at) DESC, hr.mountain_id DESC
49+
grouped.mountain_id AS mountainId,
50+
grouped.mountain_name AS mountainName,
51+
photos.image_url1 AS imageUrl1,
52+
photos.image_url2 AS imageUrl2,
53+
grouped.hiking_count AS hikingCount,
54+
grouped.last_hiked_at AS lastHikedAt
55+
FROM (
56+
SELECT
57+
hr.mountain_id,
58+
m.name AS mountain_name,
59+
m.location AS mountain_location,
60+
COUNT(hr.id) AS hiking_count,
61+
MAX(hr.created_at) AS last_hiked_at
62+
FROM hiking_records hr
63+
JOIN hiking_members hm ON hm.hiking_record_id = hr.id
64+
JOIN mountains m ON m.id = hr.mountain_id
65+
WHERE hm.user_id = :userId
66+
GROUP BY hr.mountain_id, m.name, m.location
67+
) grouped
68+
LEFT JOIN LATERAL (
69+
SELECT
70+
MAX(CASE WHEN ranked.rn = 1 THEN ranked.image_url END) AS image_url1,
71+
MAX(CASE WHEN ranked.rn = 2 THEN ranked.image_url END) AS image_url2
72+
FROM (
73+
SELECT
74+
tp.image_url,
75+
ROW_NUMBER() OVER (
76+
ORDER BY
77+
ST_Distance(
78+
grouped.mountain_location,
79+
ST_SetSRID(ST_MakePoint(tp.lng, tp.lat), 4326)::geography
80+
) ASC,
81+
tp.captured_at DESC,
82+
tp.id DESC
83+
) AS rn
84+
FROM tracking_photos tp
85+
JOIN tracking_sessions ts ON ts.id = tp.tracking_session_id
86+
WHERE ts.user_id = :userId
87+
AND ts.mountain_id = grouped.mountain_id
88+
AND ts.status = 'COMPLETED'
89+
AND grouped.mountain_location IS NOT NULL
90+
AND NULLIF(tp.image_url, '') IS NOT NULL
91+
ORDER BY
92+
ST_Distance(
93+
grouped.mountain_location,
94+
ST_SetSRID(ST_MakePoint(tp.lng, tp.lat), 4326)::geography
95+
) ASC,
96+
tp.captured_at DESC,
97+
tp.id DESC
98+
LIMIT 2
99+
) ranked
100+
) photos ON true
101+
ORDER BY grouped.last_hiked_at DESC, grouped.mountain_id DESC
60102
""",
61103
countQuery = """
62104
SELECT COUNT(DISTINCT hr.mountain_id)

src/main/java/com/semosan/api/domain/hiking/repository/projection/UserHikingMountainRecordProjection.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@ public interface UserHikingMountainRecordProjection {
88

99
String getMountainName();
1010

11-
String getImageUrl();
11+
String getImageUrl1();
12+
13+
String getImageUrl2();
1214

1315
Long getHikingCount();
1416

Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
package com.semosan.api.domain.hiking.dto.response;
2+
3+
import com.semosan.api.domain.hiking.repository.projection.UserHikingMountainRecordProjection;
4+
import org.junit.jupiter.api.Test;
5+
6+
import java.time.LocalDate;
7+
import java.time.LocalDateTime;
8+
9+
import static org.assertj.core.api.Assertions.assertThat;
10+
11+
class GetUserHikingMountainRecordResponseTest {
12+
13+
@Test
14+
void fromMapsImageUrlsInOrder() {
15+
GetUserHikingMountainRecordResponse response = GetUserHikingMountainRecordResponse.from(
16+
projection("near-summit-1", "near-summit-2")
17+
);
18+
19+
assertThat(response.imageUrls()).containsExactly("near-summit-1", "near-summit-2");
20+
assertThat(response.lastHikedAt()).isEqualTo(LocalDate.of(2026, 5, 28));
21+
}
22+
23+
@Test
24+
void fromFiltersBlankImageUrls() {
25+
GetUserHikingMountainRecordResponse response = GetUserHikingMountainRecordResponse.from(
26+
projection("", "near-summit-2")
27+
);
28+
29+
assertThat(response.imageUrls()).containsExactly("near-summit-2");
30+
}
31+
32+
@Test
33+
void fromReturnsEmptyImageUrlsWhenNoPhotos() {
34+
GetUserHikingMountainRecordResponse response = GetUserHikingMountainRecordResponse.from(
35+
projection(null, null)
36+
);
37+
38+
assertThat(response.imageUrls()).isEmpty();
39+
}
40+
41+
private UserHikingMountainRecordProjection projection(String imageUrl1, String imageUrl2) {
42+
return new UserHikingMountainRecordProjection() {
43+
@Override
44+
public Long getMountainId() {
45+
return 1L;
46+
}
47+
48+
@Override
49+
public String getMountainName() {
50+
return "관악산";
51+
}
52+
53+
@Override
54+
public String getImageUrl1() {
55+
return imageUrl1;
56+
}
57+
58+
@Override
59+
public String getImageUrl2() {
60+
return imageUrl2;
61+
}
62+
63+
@Override
64+
public Long getHikingCount() {
65+
return 3L;
66+
}
67+
68+
@Override
69+
public LocalDateTime getLastHikedAt() {
70+
return LocalDateTime.of(2026, 5, 28, 10, 0);
71+
}
72+
};
73+
}
74+
}

0 commit comments

Comments
 (0)