Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat #38 매칭 필터링 및 알고리즘 구현 #38

Open
wants to merge 25 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
eef7b34
refactor: 자동 매칭 handlerAutoRequestMatching 메서드 위도 경도 정보 사용해주도록 설정
huncozyboy Jan 16, 2025
d796daa
refactor: 300m 이내 반경 지정 위해 위도 경도 정보 사용하도록 설정
huncozyboy Jan 16, 2025
103f446
feat: 매칭방 리스트 검색 쿼리 및 중복 참여 방지 쿼리 구현
huncozyboy Jan 16, 2025
8edc548
refactor: saveRoute 메서드 위도와 경도 사용하도록 변경
huncozyboy Jan 16, 2025
43382ca
refactor: Route 엔티티 위도와 경도 필드 추가
huncozyboy Jan 16, 2025
b305484
feat: 매칭 출발지와 도착지 좌표, 태그 조건으로 필터링 메서드 구현
huncozyboy Jan 16, 2025
579a8dd
feat: 페이지 예외처리인 PageNotFoundException 구현
huncozyboy Jan 16, 2025
f29a3ba
feat: 페이지 예외 메시지 구현
huncozyboy Jan 16, 2025
2419893
feat: 페이지 리스트로 조회해오는 메서드 구현
huncozyboy Jan 16, 2025
109e7d2
feat: 인터페이스에서 findMatchingRooms 메서드 추가
huncozyboy Jan 16, 2025
18030a7
refactor: 이미 참여중인 유저 검증 로직 수정
huncozyboy Jan 16, 2025
7f4004a
feat: 태그 조건이 비어있는 경우 findRoomsByStartAndDestination 메서드 추가
huncozyboy Jan 16, 2025
add4ca9
feat: findRoomsByStartAndDestination 메서드 서비스 단에서 구현
huncozyboy Jan 16, 2025
848e35d
refactor: memberService에 있는 findById로 조회
huncozyboy Jan 16, 2025
d7fe04d
feat: containsTag 특정 태그를 포함하는 방인지 확인하는 메서드 구현
huncozyboy Jan 16, 2025
a290383
refactor: 레포에서는 거리와 관련된 정보만 조회해오도록 쿼리문 수정
huncozyboy Jan 16, 2025
dd9a668
feat: 특정 태그를 포함하는지 확인하는 matchesTag 메서드 구현
huncozyboy Jan 16, 2025
dee5512
refactor: 필터링 부분 리팩토링
huncozyboy Jan 16, 2025
53befbb
feat: 매칭방 중복 생성 예외처리 구현
huncozyboy Jan 16, 2025
4ce39e3
feat: 매칭방 중복 생성 예외 메시지 추가
huncozyboy Jan 16, 2025
f5d9b52
refactor: 동일한 유저가 똑같은 조건의 방 생성시 중복 예외처리
huncozyboy Jan 16, 2025
289fea5
refactor: 빌더패턴 MatchingRoom 엔티티 내부로 이전
huncozyboy Jan 17, 2025
68ef3a1
refactor: 서비스단 빌더 패턴 간단하게 수정
huncozyboy Jan 17, 2025
783fdd9
refactor: 300m 거리 동적으로 설정되도록 파라미터로 수정
huncozyboy Jan 17, 2025
c7889f6
refactor: SEARCH_RADIUS 거리 상수 추가
huncozyboy Jan 17, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,20 +1,33 @@
package com.gachtaxi.domain.matching.algorithm.service;

import com.gachtaxi.domain.matching.algorithm.dto.FindRoomResult;
import com.gachtaxi.domain.matching.common.entity.MatchingRoom;
import com.gachtaxi.domain.matching.common.entity.enums.Tags;
import java.util.List;
import java.util.Optional;
import org.springframework.data.domain.Page;

public interface MatchingAlgorithmService {

/**
* 방을 찾는 메서드
* 이미 방에 들어가있는 멤버가 다시 요청했을 때 Optional.empty()를 반환하도록 로직을 구성해야함
* @param userId 방에 들어가려는 사용자 ID
* @param startPoint 매칭 시작 지점 좌표
* @param destinationPoint 도착지 좌표
* @param startLongitude 시작 지점 경도
* @param startLatitude 시작 지점 위도
* @param destinationLongitude 도착 지점 경도
* @param destinationLatitude 도착 지점 위도
* @param criteria 방 검색에 필요한 기타 조건 (태그 등)
* @return Optional<FindRoomResult> - 매칭 가능한 방 정보가 있으면 값이 있고, 없으면 empty
*/
Optional<FindRoomResult> findRoom(Long userId, String startPoint, String destinationPoint, List<Tags> criteria);
Optional<FindRoomResult> findRoom(Long userId, double startLongitude, double startLatitude, double destinationLongitude, double destinationLatitude, List<Tags> criteria);

/**
* 전체 매칭 방을 페이지 단위로 조회
*
* @param pageNumber 페이지 번호 (0부터 시작)
* @param pageSize 한 페이지에 포함될 매칭 방의 개수
* @return Page<MatchingRoom> - 페이지별 매칭 방 정보
*/
Page<MatchingRoom> findMatchingRooms(int pageNumber, int pageSize);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,29 +3,85 @@
import com.gachtaxi.domain.matching.algorithm.dto.FindRoomResult;
import com.gachtaxi.domain.matching.common.entity.MatchingRoom;
import com.gachtaxi.domain.matching.common.entity.enums.Tags;
import com.gachtaxi.domain.matching.common.exception.DuplicatedMatchingRoomException;
import com.gachtaxi.domain.matching.common.exception.PageNotFoundException;
import com.gachtaxi.domain.matching.common.repository.MatchingRoomRepository;
import com.gachtaxi.domain.members.entity.Members;
import com.gachtaxi.domain.members.service.MemberService;
import java.util.List;
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Sort;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class MockMatchingAlgorithmService implements MatchingAlgorithmService {

private final MatchingRoomRepository matchingRoomRepository;
private final MemberService memberService;

private static final double SEARCH_RADIUS = 300.0;

@Override
public Optional<FindRoomResult> findRoom(Long userId, String startPoint, String destinationPoint, List<Tags> criteria) {
List<MatchingRoom> matchingRoomList = this.matchingRoomRepository.findAll();
if (!matchingRoomList.isEmpty()) {
MatchingRoom matchingRoom = matchingRoomList.get(0);
return Optional.of(
FindRoomResult.builder()
.roomId(matchingRoom.getId())
.maxCapacity(matchingRoom.getCapacity())
.build());
public Optional<FindRoomResult> findRoom(Long userId, double startLongitude, double startLatitude, double destinationLongitude, double destinationLatitude,
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

현재는 서버에 저장된 Rount(가천대 - 정문) 노선만 제공을 하고 있으니, 서버에서 제공하는 정보를 토대로 클라이언트에서 위도 경도 값을 그대로 넣어줘야할 것 같아요!

그렇다면 입력받은 값에 대한 유효성 검증이 추가되면 좋을 것 같아요. 위도, 경도 값으로 Route가 존재하는지 조회를 해본다거나 하는

List<Tags> criteria) {
/*
사용자 ID로 사용자 정보 조회(이미 방에 참여하고 있는지 중복체크)
*/
Members user = memberService.findById(userId);

if (matchingRoomRepository.existsByMemberInMatchingRoom(user)) {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

예외를 던져주게 되면 매칭 이벤트가 종료 되나요??

Copy link
Member Author

@huncozyboy huncozyboy Jan 17, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

매칭 이벤트가 종료되거나 기존 매칭방이 삭제되는 것은 아니라고 생각해주시면 됩니다. 기존의 매칭방은 정상적으로 유지되고,
만약에 사용자가 이미 생성된 매칭방에 대해 매칭 종료를 요청하거나 취소를 원할 경우는 매칭 취소와 관련된, 별도의 권한이라 매칭 취소의 프로세스에 맞다고 생각했습니다

해당 부분에 대해서는 고민을 많이 했던 부분인데 400을 던져주는 방식으로 설계한 이유는,
사용자가 이전에 설정한 조건과 동일한 조건으로 매칭방을 생성하려고 시도하는 상황에서는 이미 해당 조건에 맞는 매칭방이 존재하는 전제가 있는거니까 정상적인 흐름이 아니라고 간주하고, 이를 막아주는 것이 올바른 흐름이라고 판단하였습니다

그래서 동일한 조건으로 생성된 방이 이미 존재하는 상황에서는 중복된 방 생성을 방지하기 위해
서버단에서 예외를 던져주도록 설계하였습니다. 원래는 클라이언트 측에서
이미 참여 중인 매칭방 화면으로 유저를 리다이렉트하는 것이 더 적절하다고 생각했지만,
안정성을 고려했을 때 서버단에서도 중복 여부를 검증하는 것이 필요하다고 생각했습니다

throw new DuplicatedMatchingRoomException(); // * 추후 논의 후 리팩토링 필요 * 똑같은 조건으로 방 생성시 예외 던져주기
}
/*
위치 정보를 이용한 방 검색(300M 이내)
*/
List<MatchingRoom> matchingRooms = matchingRoomRepository.findRoomsByStartAndDestination(
startLongitude,
startLatitude,
destinationLongitude,
destinationLatitude,
SEARCH_RADIUS
);
/*
ACTIVE 상태인 방만 필터링
*/
matchingRooms = matchingRooms.stream()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

DB에서 가져올 때 Active인 방들만 가져오는 것은 어떨까요??
Jpa 메서드로도 작성이 가능하니 조회할 때 필터링을 거쳐주는 것도 성능에 좋아보여요

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

넵 원래는 Active 상태의 방과 태그 조건까지 모두 쿼리로 조회해 오도록 설계했었는데, 주영님의 의견을 반영하여 수정했습니다

해당 조건들을 모두 쿼리에서 처리하려면 쿼리가 너무 복잡하고 길어져서 서버단 성능에 부담이 갈 수도 있고, 가독성이 떨어질 수도 있다는 피드백이 있었어서, 해당 부분 고려한 뒤에 Active 상태와 태그 필터링은 서비스 단에서 처리하는 방식으로 이전하였습니다...!

.filter(MatchingRoom::isActiveMatchingRoom)
.toList();
/*
태그 조건이 있는 경우에 태그정보까지 필터링
*/
if (criteria != null && !criteria.isEmpty()) {
matchingRooms = matchingRooms.stream()
.filter(room -> criteria.stream().anyMatch(room::containsTag))
.toList();
}
/*
조건에 맞는 방이 있으면 첫 번째 방의 상세 정보 반환
*/
if (!matchingRooms.isEmpty()) {
MatchingRoom room = matchingRooms.get(0);
return Optional.of(room.toFindRoomResult());
}
/*
조건에 맞는 방이 없으면 empty 반환
*/
return Optional.empty();
}
@Override
public Page<MatchingRoom> findMatchingRooms(int pageNumber, int pageSize) {

if (pageNumber < 0) {
throw new PageNotFoundException();
}

PageRequest pageable = PageRequest.of(pageNumber, pageSize, Sort.by(Sort.Direction.DESC, "id"));

return matchingRoomRepository.findAll(pageable);
}

}
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package com.gachtaxi.domain.matching.common.entity;

import com.gachtaxi.domain.matching.algorithm.dto.FindRoomResult;
import com.gachtaxi.domain.matching.common.entity.enums.MatchingRoomStatus;
import com.gachtaxi.domain.matching.common.entity.enums.Tags;
import com.gachtaxi.domain.members.entity.Members;
import com.gachtaxi.global.common.entity.BaseEntity;
import jakarta.persistence.CascadeType;
Expand Down Expand Up @@ -59,4 +61,14 @@ public class MatchingRoom extends BaseEntity {
public boolean isActiveMatchingRoom() {
return this.matchingRoomStatus == MatchingRoomStatus.ACTIVE;
}
public boolean containsTag(Tags tag) {
return this.matchingRoomTagInfo.stream()
.anyMatch(tagInfo -> tagInfo.matchesTag(tag));
}
public FindRoomResult toFindRoomResult() {
return FindRoomResult.builder()
.roomId(this.getId())
.maxCapacity(this.getCapacity())
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,8 @@ public class MatchingRoomTagInfo extends BaseEntity {

@Enumerated(EnumType.STRING)
private Tags tags;

public boolean matchesTag(Tags tag) {
return this.tags == tag;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,11 @@
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Route extends BaseEntity {

private String startLocationCoordinate;
private double startLongitude;
private double startLatitude;
private String startLocationName;

private String endLocationCoordinate;
private double endLongitude;
private double endLatitude;
private String endLocationName;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package com.gachtaxi.domain.matching.common.exception;

import static com.gachtaxi.domain.matching.common.exception.ErrorMessage.DUPLICATED_MATCHING_ROOM;
import static org.springframework.http.HttpStatus.BAD_REQUEST;

import com.gachtaxi.global.common.exception.BaseException;

public class DuplicatedMatchingRoomException extends BaseException {
public DuplicatedMatchingRoomException() {
super(BAD_REQUEST, DUPLICATED_MATCHING_ROOM.getMessage());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,9 @@
public enum ErrorMessage {

NO_SUCH_MATCHING_ROOM("해당 매칭 방이 존재하지 않습니다."),
NOT_ACTIVE_MATCHING_ROOM("열린 매칭 방이 아닙니다.");
NOT_ACTIVE_MATCHING_ROOM("열린 매칭 방이 아닙니다."),
DUPLICATED_MATCHING_ROOM("이미 존재하는 매칭 방입니다."),
NOT_FOUND_PAGE("페이지 번호는 0 이상이어야 합니다.");

private final String message;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gachtaxi.domain.matching.common.exception;

import com.gachtaxi.global.common.exception.BaseException;
import org.springframework.http.HttpStatus;

public class PageNotFoundException extends BaseException {
public PageNotFoundException() {
super(HttpStatus.NOT_FOUND, ErrorMessage.NOT_FOUND_PAGE.getMessage());
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

정적 경로 설정이 빠진 것 같아요!

}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,28 @@
package com.gachtaxi.domain.matching.common.repository;

import com.gachtaxi.domain.matching.common.entity.MatchingRoom;
import com.gachtaxi.domain.members.entity.Members;
import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;

@Repository
public interface MatchingRoomRepository extends JpaRepository<MatchingRoom, Long> {

@Query("SELECT r FROM MatchingRoom r " +
"WHERE " +
"FUNCTION('ST_Distance_Sphere', FUNCTION('POINT', :startLongitude, :startLatitude), FUNCTION('POINT', r.route.startLongitude, r.route.startLatitude)) <= :radius " +
"AND FUNCTION('ST_Distance_Sphere', FUNCTION('POINT', :destinationLongitude, :destinationLatitude), FUNCTION('POINT', r.route.endLongitude, r.route.endLatitude)) <= :radius ")
List<MatchingRoom> findRoomsByStartAndDestination(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

거리도 파라미터로 빼주고 호출하는 쪽에서 상수로 관리해주면 거리 변경이 필요할 때 유용할 것 같아욤

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

기존 쿼리의 <= 300 부분 :radius로 설정해줘서 아래 내용처럼 상수로 선언해서 사용하겠습니당

private static final double SEARCH_RADIUS = 300.0;

@Param("startLongitude") double startLongitude,
@Param("startLatitude") double startLatitude,
@Param("destinationLongitude") double destinationLongitude,
@Param("destinationLatitude") double destinationLatitude,
@Param("radius") double radius
);
@Query("SELECT CASE WHEN COUNT(m) > 0 THEN true ELSE false END " +
"FROM MatchingRoom r JOIN r.memberMatchingRoomChargingInfo m " +
"WHERE m.members = :user")
boolean existsByMemberInMatchingRoom(@Param("user") Members user);
}
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,17 @@ public AutoMatchingPostResponse handlerAutoRequestMatching(
AutoMatchingPostRequest autoMatchingPostRequest
) {
List<Tags> criteria = autoMatchingPostRequest.getCriteria();

String[] startCoordinates = autoMatchingPostRequest.startPoint().split(",");
double startLongitude = Double.parseDouble(startCoordinates[0]);
double startLatitude = Double.parseDouble(startCoordinates[1]);

String[] destinationCoordinates = autoMatchingPostRequest.destinationPoint().split(",");
double destinationLongitude = Double.parseDouble(destinationCoordinates[0]);
double destinationLatitude = Double.parseDouble(destinationCoordinates[1]);

Optional<FindRoomResult> optionalRoom =
this.matchingAlgorithmService.findRoom(memberId, autoMatchingPostRequest.startPoint(),
autoMatchingPostRequest.destinationPoint(), criteria);
this.matchingAlgorithmService.findRoom(memberId, startLongitude, startLatitude, destinationLongitude, destinationLatitude, criteria);

optionalRoom
.ifPresentOrElse(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,13 +57,22 @@ public MatchingRoom save(MatchRoomCreatedEvent matchRoomCreatedEvent) {
}

private Route saveRoute(MatchRoomCreatedEvent matchRoomCreatedEvent) {
Route route = Route.builder()
.startLocationCoordinate(matchRoomCreatedEvent.startPoint())
.startLocationName(matchRoomCreatedEvent.startName())
.endLocationCoordinate(matchRoomCreatedEvent.destinationPoint())
.endLocationName(matchRoomCreatedEvent.destinationName())
.build();
String[] startCoordinates = matchRoomCreatedEvent.startPoint().split(",");
double startLongitude = Double.parseDouble(startCoordinates[0]);
double startLatitude = Double.parseDouble(startCoordinates[1]);

String[] endCoordinates = matchRoomCreatedEvent.destinationPoint().split(",");
double endLongitude = Double.parseDouble(endCoordinates[0]);
double endLatitude = Double.parseDouble(endCoordinates[1]);

Route route = Route.builder()
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 객체 생성의 책임도 객체 내부로 이전하면 통일성 있고 좋을 것 같아요!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 부분은 원래 구현되어 있던 내용을 그대로 사용했기 때문에 미처 확인하지 못했던 부분입니다
현재 구현한 설계들과 동일한 패턴들 처럼, Route 객체 내부로 이전하여 통일성을 유지하는 것이 저도 맞다고 생각되어서 반영하도록 하겠습니다 !

.startLongitude(startLongitude)
.startLatitude(startLatitude)
.startLocationName(matchRoomCreatedEvent.startName())
.endLongitude(endLongitude)
.endLatitude(endLatitude)
.endLocationName(matchRoomCreatedEvent.destinationName())
.build();
return this.routeRepository.save(route);
}

Expand Down
Loading