From 60d1827a6ca143ad574bbb875538a8315d90f666 Mon Sep 17 00:00:00 2001 From: Donghun Won Date: Sun, 4 Jan 2026 20:04:22 +0900 Subject: [PATCH] =?UTF-8?q?feat=20:=20=ED=8C=8C=ED=8B=B0=20=EC=B9=B4?= =?UTF-8?q?=ED=85=8C=EA=B3=A0=EB=A6=AC,=20=EC=B5=9C=EC=8B=A0=EC=88=9C,=20?= =?UTF-8?q?=EA=B1=B0=EB=A6=AC=EC=88=9C=20=EC=A0=95=EB=A0=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../party/controller/PartyController.java | 42 +++++- .../party/dto/request/PartyListRequest.java | 17 +++ .../party/dto/response/PartyCardResponse.java | 8 +- .../domain/party/enums/PartySortType.java | 15 ++ .../domain/party/service/PartyService.java | 136 +++++++++--------- .../global/util/DistanceCalculator.java | 2 +- 6 files changed, 148 insertions(+), 72 deletions(-) create mode 100644 src/main/java/ita/tinybite/domain/party/dto/request/PartyListRequest.java create mode 100644 src/main/java/ita/tinybite/domain/party/enums/PartySortType.java diff --git a/src/main/java/ita/tinybite/domain/party/controller/PartyController.java b/src/main/java/ita/tinybite/domain/party/controller/PartyController.java index 4db4ce2..fde9243 100644 --- a/src/main/java/ita/tinybite/domain/party/controller/PartyController.java +++ b/src/main/java/ita/tinybite/domain/party/controller/PartyController.java @@ -9,6 +9,7 @@ import io.swagger.v3.oas.annotations.tags.Tag; import ita.tinybite.domain.auth.entity.JwtTokenProvider; import ita.tinybite.domain.party.dto.request.PartyCreateRequest; +import ita.tinybite.domain.party.dto.request.PartyListRequest; import ita.tinybite.domain.party.dto.request.PartyQueryListResponse; import ita.tinybite.domain.party.dto.request.PartyUpdateRequest; import ita.tinybite.domain.party.dto.response.ChatRoomResponse; @@ -16,6 +17,7 @@ import ita.tinybite.domain.party.dto.response.PartyListResponse; import ita.tinybite.domain.party.entity.PartyParticipant; import ita.tinybite.domain.party.enums.PartyCategory; +import ita.tinybite.domain.party.enums.PartySortType; import ita.tinybite.domain.party.service.PartySearchService; import ita.tinybite.domain.party.service.PartyService; import ita.tinybite.global.response.APIResponse; @@ -275,10 +277,42 @@ public ResponseEntity getPartyList( example = "ALL", schema = @Schema(allowableValues = {"ALL", "DELIVERY", "GROCERY", "HOUSEHOLD"}) ) - @RequestParam(defaultValue = "ALL") PartyCategory category - ) { - PartyListResponse response = partyService.getPartyList( - userId, category); + @RequestParam(defaultValue = "ALL") PartyCategory category, + @RequestParam(required = false, defaultValue = "LATEST") PartySortType sortType, + @RequestParam(required = false) String userLat, + @RequestParam(required = false) String userLon + ) { + Double lat = null; + Double lon = null; + // 거리순 정렬 시 위치 정보 검증 + if (sortType == PartySortType.DISTANCE) { + if (userLat == null || userLon == null) { + throw new IllegalArgumentException("거리순 정렬을 위해서는 현재 위치 정보가 필요합니다."); + } + + try { + lat = Double.parseDouble(userLat); + lon = Double.parseDouble(userLon); + } catch (NumberFormatException e) { + throw new IllegalArgumentException( + String.format("위치 정보 형식이 올바르지 않습니다. userLat: %s, userLon: %s", userLat, userLon) + ); + } + + // 위도/경도 범위 검증 + if (lat < -90 || lat > 90 || lon < -180 || lon > 180) { + throw new IllegalArgumentException("위도/경도 값이 유효한 범위를 벗어났습니다."); + } + } + + PartyListRequest request = PartyListRequest.builder() + .category(category) + .sortType(sortType) + .userLat(lat) + .userLon(lon) + .build(); + + PartyListResponse response = partyService.getPartyList(userId, request); return ResponseEntity.ok(response); } diff --git a/src/main/java/ita/tinybite/domain/party/dto/request/PartyListRequest.java b/src/main/java/ita/tinybite/domain/party/dto/request/PartyListRequest.java new file mode 100644 index 0000000..c87a06b --- /dev/null +++ b/src/main/java/ita/tinybite/domain/party/dto/request/PartyListRequest.java @@ -0,0 +1,17 @@ +package ita.tinybite.domain.party.dto.request; + +import ita.tinybite.domain.party.enums.PartyCategory; +import ita.tinybite.domain.party.enums.PartySortType; +import lombok.Builder; +import lombok.Getter; + +@Getter +@Builder +public class PartyListRequest { + private PartyCategory category; // 필터: 카테고리 + private PartySortType sortType; // 정렬: 최신순/거리순 + + // 거리순 정렬을 위한 현재 위치 (선택) + private Double userLat; + private Double userLon; +} diff --git a/src/main/java/ita/tinybite/domain/party/dto/response/PartyCardResponse.java b/src/main/java/ita/tinybite/domain/party/dto/response/PartyCardResponse.java index 8edc875..11b97c0 100644 --- a/src/main/java/ita/tinybite/domain/party/dto/response/PartyCardResponse.java +++ b/src/main/java/ita/tinybite/domain/party/dto/response/PartyCardResponse.java @@ -3,6 +3,7 @@ import ita.tinybite.domain.party.enums.ParticipantStatus; import ita.tinybite.domain.party.enums.PartyCategory; import ita.tinybite.domain.party.enums.PartyStatus; +import ita.tinybite.global.util.DistanceCalculator; import lombok.*; import java.time.Duration; @@ -20,7 +21,7 @@ public class PartyCardResponse { private Integer pricePerPerson; // 1/N 가격 private String participantStatus; // "1/4명" private String distance; // "300m" or "1.2km" (화면 표시용) - private Double distanceKm; // km 단위 거리 (정렬용) + private String distanceKm; // km 단위 거리 (정렬용) private String timeAgo; // "10분 전", "3시간 전" private Boolean isClosed; // 마감 여부 private PartyCategory category; @@ -109,4 +110,9 @@ private static Boolean checkIfClosed(Party party, int currentParticipants) { return false; } + + public void addDistanceKm(Double distance) { + this.distanceKm = DistanceCalculator.formatDistance(distance); + this.distance = Double.toString(distance); + } } \ No newline at end of file diff --git a/src/main/java/ita/tinybite/domain/party/enums/PartySortType.java b/src/main/java/ita/tinybite/domain/party/enums/PartySortType.java new file mode 100644 index 0000000..7bb8005 --- /dev/null +++ b/src/main/java/ita/tinybite/domain/party/enums/PartySortType.java @@ -0,0 +1,15 @@ +package ita.tinybite.domain.party.enums; + +import lombok.Getter; + +@Getter +public enum PartySortType { + LATEST("최신순"), + DISTANCE("거리순"); + + private final String description; + + PartySortType(String description) { + this.description = description; + } +} \ No newline at end of file diff --git a/src/main/java/ita/tinybite/domain/party/service/PartyService.java b/src/main/java/ita/tinybite/domain/party/service/PartyService.java index 646c5bf..2bf194e 100644 --- a/src/main/java/ita/tinybite/domain/party/service/PartyService.java +++ b/src/main/java/ita/tinybite/domain/party/service/PartyService.java @@ -4,7 +4,7 @@ import ita.tinybite.domain.chat.enums.ChatRoomType; import ita.tinybite.domain.chat.repository.ChatRoomRepository; import ita.tinybite.domain.party.dto.request.PartyCreateRequest; -import ita.tinybite.domain.party.dto.request.PartyQueryListResponse; +import ita.tinybite.domain.party.dto.request.PartyListRequest; import ita.tinybite.domain.party.dto.request.PartyUpdateRequest; import ita.tinybite.domain.party.dto.response.*; import ita.tinybite.domain.party.entity.Party; @@ -12,25 +12,23 @@ import ita.tinybite.domain.party.entity.PickupLocation; import ita.tinybite.domain.party.enums.ParticipantStatus; import ita.tinybite.domain.party.enums.PartyCategory; +import ita.tinybite.domain.party.enums.PartySortType; import ita.tinybite.domain.party.enums.PartyStatus; import ita.tinybite.domain.party.repository.PartyParticipantRepository; import ita.tinybite.domain.party.repository.PartyRepository; import ita.tinybite.domain.user.entity.User; import ita.tinybite.domain.user.repository.UserRepository; - -import java.time.LocalDateTime; -import java.util.List; -import java.util.stream.Collectors; - import ita.tinybite.global.location.LocationService; import ita.tinybite.global.util.DistanceCalculator; import lombok.RequiredArgsConstructor; -import org.springframework.data.domain.Page; -import org.springframework.data.domain.PageRequest; -import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.time.LocalDateTime; +import java.util.Comparator; +import java.util.List; +import java.util.stream.Collectors; + @Service @RequiredArgsConstructor @@ -42,6 +40,7 @@ public class PartyService { private final PartyParticipantRepository partyParticipantRepository; private final ChatRoomRepository chatRoomRepository; private final PartyParticipantRepository participantRepository; + /** * 파티 생성 */ @@ -105,72 +104,43 @@ public Long createParty(Long userId, PartyCreateRequest request) { /** * 파티 목록 조회 (홈 화면) */ - public PartyListResponse getPartyList(Long userId, PartyCategory category) { + public PartyListResponse getPartyList(Long userId, PartyListRequest request) { User user = null; if (userId != null) { user = userRepository.findById(userId).orElse(null); } // 동네 기준으로 파티 조회 - List parties = List.of(); - if (user != null && user.getLocation() != null) { - if (category == PartyCategory.ALL) { - parties = partyRepository.findByPickupLocation_Place(user.getLocation()); - } else { - parties = partyRepository.findByPickupLocation_PlaceAndCategory( - user.getLocation(), category); - } - } -// else { -// // 비회원이거나 동네 미설정 시 -// String location = locationService.getLocation(userLat, userLon); -// if (category == PartyCategory.ALL) { -// parties = partyRepository.findByPickupLocation_Place(location); -// } else { -// parties = partyRepository.findByPickupLocation_PlaceAndCategory( -// location, category); -// } -// } - -// List cardResponses = parties.stream() -// .map(party -> { -// // DistanceCalculator 활용 -// double distance = DistanceCalculator.calculateDistance( -// Double.parseDouble(userLat), Double.parseDouble(userLon), -// party.getLatitude(), party.getLongitude() -// ); -// return convertToCardResponse(party, distance, userId, party.getCreatedAt()); -// }) -// .collect(Collectors.toList()); -// -// // 진행 중 파티: 거리 가까운 순 정렬 -// List activeParties = cardResponses.stream() -// .filter(p -> !p.getIsClosed()) -// .sorted((a, b) -> Double.compare(a.getDistanceKm(), b.getDistanceKm())) -// .collect(Collectors.toList()); -// -// // 마감된 파티: 거리 가까운 순 정렬 -// List closedParties = cardResponses.stream() -// .filter(PartyCardResponse::getIsClosed) -// .sorted((a, b) -> Double.compare(a.getDistanceKm(), b.getDistanceKm())) -// .collect(Collectors.toList()); - + List parties = fetchPartiesByLocation(user, request); + // PartyCardResponse로 변환 List cardResponses = parties.stream() - .map(party -> convertToCardResponse(party, userId, party.getCreatedAt())) + .map(party -> { + // 거리순 정렬인 경우 거리 계산 + if (request.getSortType() == PartySortType.DISTANCE) { + double distance = DistanceCalculator.calculateDistance( + request.getUserLat(), + request.getUserLon(), + party.getPickupLocation().getPickupLatitude(), + party.getPickupLocation().getPickupLongitude() + ); + return convertToCardResponseWithDistance(party, distance); + } + return convertToCardResponse(party,party.getCreatedAt()); + }) + .toList(); + + // 진행 중 파티 정렬 + List activeParties = cardResponses.stream() + .filter(p -> !p.getIsClosed()) + .sorted(getComparator(request.getSortType())) .collect(Collectors.toList()); - // 진행 중 파티: 최신순 정렬 (createdAt 기준 내림차순) - List activeParties = cardResponses.stream() - .filter(p -> !p.getIsClosed()) - .sorted((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt())) - .collect(Collectors.toList()); - - // 마감된 파티: 최신순 정렬 (createdAt 기준 내림차순) - List closedParties = cardResponses.stream() - .filter(PartyCardResponse::getIsClosed) - .sorted((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt())) - .collect(Collectors.toList()); + // 마감된 파티 정렬 + List closedParties = cardResponses.stream() + .filter(PartyCardResponse::getIsClosed) + .sorted(getComparator(request.getSortType())) + .collect(Collectors.toList()); return PartyListResponse.builder() .activeParties(activeParties) @@ -272,7 +242,7 @@ private String getDefaultImageIfEmpty(List images, PartyCategory categor }; } - private PartyCardResponse convertToCardResponse(Party party, Long userId, + private PartyCardResponse convertToCardResponse(Party party, LocalDateTime createdAt) { int pricePerPerson = party.getPrice() / party.getMaxParticipants(); String participantStatus = party.getCurrentParticipants() + "/" @@ -675,6 +645,40 @@ private String formatDistanceIfExists(Double distance) { return distance!= null? DistanceCalculator.formatDistance(distance):null; } + //카테고리에 따라 파티 조회 + private List fetchPartiesByLocation(User user, PartyListRequest request) { + if (user == null || user.getLocation() == null) { + return List.of(); + } + + String location = user.getLocation(); + PartyCategory category = request.getCategory(); + if (category == PartyCategory.ALL) { + return partyRepository.findByPickupLocation_Place(location); + } else { + return partyRepository.findByPickupLocation_PlaceAndCategory(location, category); + } + } + + // 정렬 기준에 따른 Comparator 반환 + private Comparator getComparator(PartySortType sortType) { + if (sortType == PartySortType.DISTANCE) { + // 거리 가까운 순 + return Comparator.comparing(PartyCardResponse::getDistanceKm) + .thenComparing((a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt())); + } else { + // 최신순 (createdAt 내림차순) + return (a, b) -> b.getCreatedAt().compareTo(a.getCreatedAt()); + } + } + + // 거리 정보 포함 변환 + private PartyCardResponse convertToCardResponseWithDistance( + Party party, Double distance) { + PartyCardResponse response = convertToCardResponse(party, party.getCreatedAt()); + response.addDistanceKm(distance); + return response; + } } diff --git a/src/main/java/ita/tinybite/global/util/DistanceCalculator.java b/src/main/java/ita/tinybite/global/util/DistanceCalculator.java index 0e41559..28c9009 100644 --- a/src/main/java/ita/tinybite/global/util/DistanceCalculator.java +++ b/src/main/java/ita/tinybite/global/util/DistanceCalculator.java @@ -43,7 +43,7 @@ public static double calculateDistanceInMeters(double lat1, double lon1, public static String formatDistance(double distanceKm) { if (distanceKm < 1.0) { // 1km 미만: 미터 단위로 표시 (반올림) - return Math.round(distanceKm * 1000) + "m"; + return String.format("%.1fkm", distanceKm); } else { // 1km 이상: km 단위로 표시 (소수점 1자리) return String.format("%.1fkm", distanceKm);