Skip to content

Commit d4e760f

Browse files
authored
Release : 파티 목록 카테고리, 최신순, 거리순 정렬 (v.1.2.0) (#84)
1 parent 16104d0 commit d4e760f

File tree

6 files changed

+148
-72
lines changed

6 files changed

+148
-72
lines changed

src/main/java/ita/tinybite/domain/party/controller/PartyController.java

Lines changed: 38 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,13 +9,15 @@
99
import io.swagger.v3.oas.annotations.tags.Tag;
1010
import ita.tinybite.domain.auth.entity.JwtTokenProvider;
1111
import ita.tinybite.domain.party.dto.request.PartyCreateRequest;
12+
import ita.tinybite.domain.party.dto.request.PartyListRequest;
1213
import ita.tinybite.domain.party.dto.request.PartyQueryListResponse;
1314
import ita.tinybite.domain.party.dto.request.PartyUpdateRequest;
1415
import ita.tinybite.domain.party.dto.response.ChatRoomResponse;
1516
import ita.tinybite.domain.party.dto.response.PartyDetailResponse;
1617
import ita.tinybite.domain.party.dto.response.PartyListResponse;
1718
import ita.tinybite.domain.party.entity.PartyParticipant;
1819
import ita.tinybite.domain.party.enums.PartyCategory;
20+
import ita.tinybite.domain.party.enums.PartySortType;
1921
import ita.tinybite.domain.party.service.PartySearchService;
2022
import ita.tinybite.domain.party.service.PartyService;
2123
import ita.tinybite.global.response.APIResponse;
@@ -275,10 +277,42 @@ public ResponseEntity<PartyListResponse> getPartyList(
275277
example = "ALL",
276278
schema = @Schema(allowableValues = {"ALL", "DELIVERY", "GROCERY", "HOUSEHOLD"})
277279
)
278-
@RequestParam(defaultValue = "ALL") PartyCategory category
279-
) {
280-
PartyListResponse response = partyService.getPartyList(
281-
userId, category);
280+
@RequestParam(defaultValue = "ALL") PartyCategory category,
281+
@RequestParam(required = false, defaultValue = "LATEST") PartySortType sortType,
282+
@RequestParam(required = false) String userLat,
283+
@RequestParam(required = false) String userLon
284+
) {
285+
Double lat = null;
286+
Double lon = null;
287+
// 거리순 정렬 시 위치 정보 검증
288+
if (sortType == PartySortType.DISTANCE) {
289+
if (userLat == null || userLon == null) {
290+
throw new IllegalArgumentException("거리순 정렬을 위해서는 현재 위치 정보가 필요합니다.");
291+
}
292+
293+
try {
294+
lat = Double.parseDouble(userLat);
295+
lon = Double.parseDouble(userLon);
296+
} catch (NumberFormatException e) {
297+
throw new IllegalArgumentException(
298+
String.format("위치 정보 형식이 올바르지 않습니다. userLat: %s, userLon: %s", userLat, userLon)
299+
);
300+
}
301+
302+
// 위도/경도 범위 검증
303+
if (lat < -90 || lat > 90 || lon < -180 || lon > 180) {
304+
throw new IllegalArgumentException("위도/경도 값이 유효한 범위를 벗어났습니다.");
305+
}
306+
}
307+
308+
PartyListRequest request = PartyListRequest.builder()
309+
.category(category)
310+
.sortType(sortType)
311+
.userLat(lat)
312+
.userLon(lon)
313+
.build();
314+
315+
PartyListResponse response = partyService.getPartyList(userId, request);
282316

283317
return ResponseEntity.ok(response);
284318
}
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
package ita.tinybite.domain.party.dto.request;
2+
3+
import ita.tinybite.domain.party.enums.PartyCategory;
4+
import ita.tinybite.domain.party.enums.PartySortType;
5+
import lombok.Builder;
6+
import lombok.Getter;
7+
8+
@Getter
9+
@Builder
10+
public class PartyListRequest {
11+
private PartyCategory category; // 필터: 카테고리
12+
private PartySortType sortType; // 정렬: 최신순/거리순
13+
14+
// 거리순 정렬을 위한 현재 위치 (선택)
15+
private Double userLat;
16+
private Double userLon;
17+
}

src/main/java/ita/tinybite/domain/party/dto/response/PartyCardResponse.java

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
import ita.tinybite.domain.party.enums.ParticipantStatus;
44
import ita.tinybite.domain.party.enums.PartyCategory;
55
import ita.tinybite.domain.party.enums.PartyStatus;
6+
import ita.tinybite.global.util.DistanceCalculator;
67
import lombok.*;
78

89
import java.time.Duration;
@@ -20,7 +21,7 @@ public class PartyCardResponse {
2021
private Integer pricePerPerson; // 1/N 가격
2122
private String participantStatus; // "1/4명"
2223
private String distance; // "300m" or "1.2km" (화면 표시용)
23-
private Double distanceKm; // km 단위 거리 (정렬용)
24+
private String distanceKm; // km 단위 거리 (정렬용)
2425
private String timeAgo; // "10분 전", "3시간 전"
2526
private Boolean isClosed; // 마감 여부
2627
private PartyCategory category;
@@ -109,4 +110,9 @@ private static Boolean checkIfClosed(Party party, int currentParticipants) {
109110

110111
return false;
111112
}
113+
114+
public void addDistanceKm(Double distance) {
115+
this.distanceKm = DistanceCalculator.formatDistance(distance);
116+
this.distance = Double.toString(distance);
117+
}
112118
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package ita.tinybite.domain.party.enums;
2+
3+
import lombok.Getter;
4+
5+
@Getter
6+
public enum PartySortType {
7+
LATEST("최신순"),
8+
DISTANCE("거리순");
9+
10+
private final String description;
11+
12+
PartySortType(String description) {
13+
this.description = description;
14+
}
15+
}

src/main/java/ita/tinybite/domain/party/service/PartyService.java

Lines changed: 70 additions & 66 deletions
Original file line numberDiff line numberDiff line change
@@ -4,33 +4,31 @@
44
import ita.tinybite.domain.chat.enums.ChatRoomType;
55
import ita.tinybite.domain.chat.repository.ChatRoomRepository;
66
import ita.tinybite.domain.party.dto.request.PartyCreateRequest;
7-
import ita.tinybite.domain.party.dto.request.PartyQueryListResponse;
7+
import ita.tinybite.domain.party.dto.request.PartyListRequest;
88
import ita.tinybite.domain.party.dto.request.PartyUpdateRequest;
99
import ita.tinybite.domain.party.dto.response.*;
1010
import ita.tinybite.domain.party.entity.Party;
1111
import ita.tinybite.domain.party.entity.PartyParticipant;
1212
import ita.tinybite.domain.party.entity.PickupLocation;
1313
import ita.tinybite.domain.party.enums.ParticipantStatus;
1414
import ita.tinybite.domain.party.enums.PartyCategory;
15+
import ita.tinybite.domain.party.enums.PartySortType;
1516
import ita.tinybite.domain.party.enums.PartyStatus;
1617
import ita.tinybite.domain.party.repository.PartyParticipantRepository;
1718
import ita.tinybite.domain.party.repository.PartyRepository;
1819
import ita.tinybite.domain.user.entity.User;
1920
import ita.tinybite.domain.user.repository.UserRepository;
20-
21-
import java.time.LocalDateTime;
22-
import java.util.List;
23-
import java.util.stream.Collectors;
24-
2521
import ita.tinybite.global.location.LocationService;
2622
import ita.tinybite.global.util.DistanceCalculator;
2723
import lombok.RequiredArgsConstructor;
28-
import org.springframework.data.domain.Page;
29-
import org.springframework.data.domain.PageRequest;
30-
import org.springframework.data.domain.Pageable;
3124
import org.springframework.stereotype.Service;
3225
import org.springframework.transaction.annotation.Transactional;
3326

27+
import java.time.LocalDateTime;
28+
import java.util.Comparator;
29+
import java.util.List;
30+
import java.util.stream.Collectors;
31+
3432

3533
@Service
3634
@RequiredArgsConstructor
@@ -42,6 +40,7 @@ public class PartyService {
4240
private final PartyParticipantRepository partyParticipantRepository;
4341
private final ChatRoomRepository chatRoomRepository;
4442
private final PartyParticipantRepository participantRepository;
43+
4544
/**
4645
* 파티 생성
4746
*/
@@ -105,72 +104,43 @@ public Long createParty(Long userId, PartyCreateRequest request) {
105104
/**
106105
* 파티 목록 조회 (홈 화면)
107106
*/
108-
public PartyListResponse getPartyList(Long userId, PartyCategory category) {
107+
public PartyListResponse getPartyList(Long userId, PartyListRequest request) {
109108
User user = null;
110109
if (userId != null) {
111110
user = userRepository.findById(userId).orElse(null);
112111
}
113112

114113
// 동네 기준으로 파티 조회
115-
List<Party> parties = List.of();
116-
if (user != null && user.getLocation() != null) {
117-
if (category == PartyCategory.ALL) {
118-
parties = partyRepository.findByPickupLocation_Place(user.getLocation());
119-
} else {
120-
parties = partyRepository.findByPickupLocation_PlaceAndCategory(
121-
user.getLocation(), category);
122-
}
123-
}
124-
// else {
125-
// // 비회원이거나 동네 미설정 시
126-
// String location = locationService.getLocation(userLat, userLon);
127-
// if (category == PartyCategory.ALL) {
128-
// parties = partyRepository.findByPickupLocation_Place(location);
129-
// } else {
130-
// parties = partyRepository.findByPickupLocation_PlaceAndCategory(
131-
// location, category);
132-
// }
133-
// }
134-
135-
// List<PartyCardResponse> cardResponses = parties.stream()
136-
// .map(party -> {
137-
// // DistanceCalculator 활용
138-
// double distance = DistanceCalculator.calculateDistance(
139-
// Double.parseDouble(userLat), Double.parseDouble(userLon),
140-
// party.getLatitude(), party.getLongitude()
141-
// );
142-
// return convertToCardResponse(party, distance, userId, party.getCreatedAt());
143-
// })
144-
// .collect(Collectors.toList());
145-
//
146-
// // 진행 중 파티: 거리 가까운 순 정렬
147-
// List<PartyCardResponse> activeParties = cardResponses.stream()
148-
// .filter(p -> !p.getIsClosed())
149-
// .sorted((a, b) -> Double.compare(a.getDistanceKm(), b.getDistanceKm()))
150-
// .collect(Collectors.toList());
151-
//
152-
// // 마감된 파티: 거리 가까운 순 정렬
153-
// List<PartyCardResponse> closedParties = cardResponses.stream()
154-
// .filter(PartyCardResponse::getIsClosed)
155-
// .sorted((a, b) -> Double.compare(a.getDistanceKm(), b.getDistanceKm()))
156-
// .collect(Collectors.toList());
157-
114+
List<Party> parties = fetchPartiesByLocation(user, request);
158115

116+
// PartyCardResponse로 변환
159117
List<PartyCardResponse> cardResponses = parties.stream()
160-
.map(party -> convertToCardResponse(party, userId, party.getCreatedAt()))
118+
.map(party -> {
119+
// 거리순 정렬인 경우 거리 계산
120+
if (request.getSortType() == PartySortType.DISTANCE) {
121+
double distance = DistanceCalculator.calculateDistance(
122+
request.getUserLat(),
123+
request.getUserLon(),
124+
party.getPickupLocation().getPickupLatitude(),
125+
party.getPickupLocation().getPickupLongitude()
126+
);
127+
return convertToCardResponseWithDistance(party, distance);
128+
}
129+
return convertToCardResponse(party,party.getCreatedAt());
130+
})
131+
.toList();
132+
133+
// 진행 중 파티 정렬
134+
List<PartyCardResponse> activeParties = cardResponses.stream()
135+
.filter(p -> !p.getIsClosed())
136+
.sorted(getComparator(request.getSortType()))
161137
.collect(Collectors.toList());
162138

163-
// 진행 중 파티: 최신순 정렬 (createdAt 기준 내림차순)
164-
List<PartyCardResponse> activeParties = cardResponses.stream()
165-
.filter(p -> !p.getIsClosed())
166-
.sorted((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt()))
167-
.collect(Collectors.toList());
168-
169-
// 마감된 파티: 최신순 정렬 (createdAt 기준 내림차순)
170-
List<PartyCardResponse> closedParties = cardResponses.stream()
171-
.filter(PartyCardResponse::getIsClosed)
172-
.sorted((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt()))
173-
.collect(Collectors.toList());
139+
// 마감된 파티 정렬
140+
List<PartyCardResponse> closedParties = cardResponses.stream()
141+
.filter(PartyCardResponse::getIsClosed)
142+
.sorted(getComparator(request.getSortType()))
143+
.collect(Collectors.toList());
174144

175145
return PartyListResponse.builder()
176146
.activeParties(activeParties)
@@ -272,7 +242,7 @@ private String getDefaultImageIfEmpty(List<String> images, PartyCategory categor
272242
};
273243
}
274244

275-
private PartyCardResponse convertToCardResponse(Party party, Long userId,
245+
private PartyCardResponse convertToCardResponse(Party party,
276246
LocalDateTime createdAt) {
277247
int pricePerPerson = party.getPrice() / party.getMaxParticipants();
278248
String participantStatus = party.getCurrentParticipants() + "/"
@@ -675,6 +645,40 @@ private String formatDistanceIfExists(Double distance) {
675645
return distance!= null? DistanceCalculator.formatDistance(distance):null;
676646
}
677647

648+
//카테고리에 따라 파티 조회
649+
private List<Party> fetchPartiesByLocation(User user, PartyListRequest request) {
650+
if (user == null || user.getLocation() == null) {
651+
return List.of();
652+
}
653+
654+
String location = user.getLocation();
655+
PartyCategory category = request.getCategory();
678656

657+
if (category == PartyCategory.ALL) {
658+
return partyRepository.findByPickupLocation_Place(location);
659+
} else {
660+
return partyRepository.findByPickupLocation_PlaceAndCategory(location, category);
661+
}
662+
}
663+
664+
// 정렬 기준에 따른 Comparator 반환
665+
private Comparator<PartyCardResponse> getComparator(PartySortType sortType) {
666+
if (sortType == PartySortType.DISTANCE) {
667+
// 거리 가까운 순
668+
return Comparator.comparing(PartyCardResponse::getDistanceKm)
669+
.thenComparing((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt()));
670+
} else {
671+
// 최신순 (createdAt 내림차순)
672+
return (a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt());
673+
}
674+
}
675+
676+
// 거리 정보 포함 변환
677+
private PartyCardResponse convertToCardResponseWithDistance(
678+
Party party, Double distance) {
679+
PartyCardResponse response = convertToCardResponse(party, party.getCreatedAt());
680+
response.addDistanceKm(distance);
681+
return response;
682+
}
679683
}
680684

src/main/java/ita/tinybite/global/util/DistanceCalculator.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ public static double calculateDistanceInMeters(double lat1, double lon1,
4343
public static String formatDistance(double distanceKm) {
4444
if (distanceKm < 1.0) {
4545
// 1km 미만: 미터 단위로 표시 (반올림)
46-
return Math.round(distanceKm * 1000) + "m";
46+
return String.format("%.1fkm", distanceKm);
4747
} else {
4848
// 1km 이상: km 단위로 표시 (소수점 1자리)
4949
return String.format("%.1fkm", distanceKm);

0 commit comments

Comments
 (0)