Skip to content

Commit

Permalink
feat: 최소 시간 보장 추천 로직 추가
Browse files Browse the repository at this point in the history
  • Loading branch information
ikjo39 committed Oct 13, 2024
1 parent a0e4465 commit d7e05e8
Show file tree
Hide file tree
Showing 9 changed files with 231 additions and 38 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,10 +52,13 @@ public MomoApiResponse<AttendeeScheduleResponse> findMySchedule(@PathVariable St

@GetMapping("/api/v1/meetings/{uuid}/recommended-schedules")
public MomoApiResponse<RecommendedSchedulesResponse> recommendSchedules(
@PathVariable String uuid, @RequestParam String recommendType, @RequestParam List<String> attendeeNames
@PathVariable String uuid,
@RequestParam String recommendType,
@RequestParam List<String> attendeeNames,
@RequestParam(value = "minTime", defaultValue = "0") int minTime
) {
RecommendedSchedulesResponse response = scheduleService.recommendSchedules(
uuid, recommendType, attendeeNames
uuid, recommendType, attendeeNames, minTime
);
return new MomoApiResponse<>(response);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -98,9 +98,11 @@ MomoApiResponse<AttendeeScheduleResponse> findMySchedule(
@ApiSuccessResponse.Ok("추천 일정 조회 성공")
MomoApiResponse<RecommendedSchedulesResponse> recommendSchedules(
@PathVariable @Schema(description = "약속 UUID") String uuid,
@RequestParam @Schema(description = "추천 기준(이른 시간 순 / 길게 볼 수 있는 순)", example = "earliest")
@RequestParam @Schema(description = "추천 기준(이른 시간 순 / 길게 볼 수 있는 순 / 최소 시간 보장 순)", example = "earliest")
String recommendType,
@RequestParam @Schema(description = "추천 대상 참여자 이름", example = "페드로, 재즈, 모모")
List<String> attendeeNames
List<String> attendeeNames,
@RequestParam @Schema(description = "최소 만남 시간(30분 단위의 분)", example = "0, 30, 60, 90")
int minTime
);
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,10 +9,12 @@
import kr.momo.domain.attendee.AttendeeGroup;
import kr.momo.domain.schedule.DateTimeInterval;
import kr.momo.domain.schedule.RecommendInterval;
import kr.momo.exception.MomoException;
import kr.momo.exception.code.ScheduleErrorCode;

public record CandidateSchedule(
RecommendInterval dateTimeInterval, AttendeeGroup attendeeGroup
) {
public record CandidateSchedule(RecommendInterval dateTimeInterval, AttendeeGroup attendeeGroup) {

private static final int MINIMUM_MIN_SIZE = 0;

public static CandidateSchedule of(
LocalDateTime startDateTime, LocalDateTime endDateTime, AttendeeGroup attendeeGroup
Expand All @@ -22,7 +24,8 @@ public static CandidateSchedule of(

public static List<CandidateSchedule> mergeContinuous(
List<CandidateSchedule> sortedSchedules,
BiPredicate<CandidateSchedule, CandidateSchedule> isContinuous
BiPredicate<CandidateSchedule, CandidateSchedule> isContinuous,
int minSize
) {
List<CandidateSchedule> mergedSchedules = new ArrayList<>();
int idx = 0;
Expand All @@ -32,14 +35,26 @@ public static List<CandidateSchedule> mergeContinuous(
.takeWhile(i -> i == headIdx || isSequential(i, sortedSchedules, isContinuous))
.map(sortedSchedules::get)
.toList();
subList.stream()
.reduce(CandidateSchedule::merge)
.ifPresent(mergedSchedules::add);
addWhenLongerOrEqualThanMinTime(subList, mergedSchedules, minSize);
idx += subList.size();
}
return mergedSchedules;
}

private static void addWhenLongerOrEqualThanMinTime(
List<CandidateSchedule> subList, List<CandidateSchedule> mergedSchedules, int minSize
) {
if (minSize < MINIMUM_MIN_SIZE) {
throw new MomoException(ScheduleErrorCode.INVALID_MIN_TIME);
}
if (minSize > subList.size()) {
return;
}
subList.stream()
.reduce(CandidateSchedule::merge)
.ifPresent(mergedSchedules::add);
}

private static boolean isSequential(
int idx,
List<CandidateSchedule> sortedSchedules,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,9 @@ public AttendeeScheduleResponse findMySchedule(String uuid, long attendeeId) {
}

@Transactional(readOnly = true)
public RecommendedSchedulesResponse recommendSchedules(String uuid, String recommendType, List<String> names) {
public RecommendedSchedulesResponse recommendSchedules(
String uuid, String recommendType, List<String> names, int minimumTime
) {
Meeting meeting = meetingRepository.findByUuid(uuid)
.orElseThrow(() -> new MomoException(MeetingErrorCode.NOT_FOUND_MEETING));
AttendeeGroup attendeeGroup = new AttendeeGroup(attendeeRepository.findAllByMeeting(meeting));
Expand All @@ -131,11 +133,13 @@ public RecommendedSchedulesResponse recommendSchedules(String uuid, String recom
ScheduleRecommender recommender = scheduleRecommenderFactory.getRecommenderOf(
attendeeGroup, filteredGroup
);
List<CandidateSchedule> recommendedResult = recommender.recommend(filteredGroup, recommendType,
meeting.getType());
List<CandidateSchedule> recommendedResult = recommender.recommend(
filteredGroup, recommendType, meeting.getType(), minimumTime
);

List<RecommendedScheduleResponse> scheduleResponses = RecommendedScheduleResponse.fromCandidateSchedules(
recommendedResult);
recommendedResult
);
return RecommendedSchedulesResponse.of(meeting.getType(), scheduleResponses);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,19 +14,24 @@
@RequiredArgsConstructor
public abstract class ScheduleRecommender {

private static final int HALF_HOUR_INTERVAL = 30;

protected final ScheduleRepository scheduleRepository;

public List<CandidateSchedule> recommend(AttendeeGroup group, String recommendType, MeetingType meetingType) {
List<CandidateSchedule> mergedCandidateSchedules = calcCandidateSchedules(group, meetingType);
public List<CandidateSchedule> recommend(
AttendeeGroup group, String recommendType, MeetingType meetingType, int minTime
) {
int minSize = minTime / HALF_HOUR_INTERVAL;
List<CandidateSchedule> mergedCandidateSchedules = calcCandidateSchedules(group, meetingType, minSize);
sortSchedules(mergedCandidateSchedules, recommendType);
return mergedCandidateSchedules.stream()
.limit(getMaxRecommendCount())
.toList();
}

private List<CandidateSchedule> calcCandidateSchedules(AttendeeGroup group, MeetingType type) {
private List<CandidateSchedule> calcCandidateSchedules(AttendeeGroup group, MeetingType type, int minSize) {
List<CandidateSchedule> intersectedDateTimes = extractProperSortedDiscreteScheduleOf(group, type);
return CandidateSchedule.mergeContinuous(intersectedDateTimes, this::isContinuous);
return CandidateSchedule.mergeContinuous(intersectedDateTimes, this::isContinuous, minSize);
}

private void sortSchedules(List<CandidateSchedule> mergedCandidateSchedules, String recommendType) {
Expand All @@ -35,7 +40,9 @@ private void sortSchedules(List<CandidateSchedule> mergedCandidateSchedules, Str
sorter.sort(mergedCandidateSchedules);
}

protected abstract List<CandidateSchedule> extractProperSortedDiscreteScheduleOf(AttendeeGroup group, MeetingType type);
protected abstract List<CandidateSchedule> extractProperSortedDiscreteScheduleOf(
AttendeeGroup group, MeetingType type
);

protected abstract boolean isContinuous(CandidateSchedule current, CandidateSchedule next);

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ void findMySchedule() {
void recommendSchedules() {
RestAssured.given().log().all()
.pathParam("uuid", meeting.getUuid())
.queryParams("recommendType", EARLIEST_ORDER.getType(), "attendeeNames", attendee.name())
.queryParams("recommendType", EARLIEST_ORDER.getType(), "attendeeNames", attendee.name(), "minimumTime", 0)
.contentType(ContentType.JSON)
.when().get("/api/v1/meetings/{uuid}/recommended-schedules")
.then().log().all()
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package kr.momo.domain.schedule.recommend;

import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.junit.jupiter.api.Assertions.assertAll;

import java.time.LocalDate;
Expand All @@ -9,20 +10,28 @@
import kr.momo.domain.attendee.AttendeeGroup;
import kr.momo.domain.schedule.DateAndTimeslot;
import kr.momo.domain.timeslot.Timeslot;
import kr.momo.exception.MomoException;
import kr.momo.exception.code.ScheduleErrorCode;
import kr.momo.fixture.AttendeeGroupFixture;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;

class CandidateScheduleTest {

private static final int DEFAULT_MIN_SIZE = 0;

@DisplayName("빈 리스트를 병합할 경우 빈 리스트를 반환한다.")
@Test
void mergeEmptyListTest() {
// given
List<CandidateSchedule> schedules = List.of();

// when
List<CandidateSchedule> mergedSchedules = CandidateSchedule.mergeContinuous(schedules, this::isContinuous);
List<CandidateSchedule> mergedSchedules = CandidateSchedule.mergeContinuous(
schedules, this::isContinuous, DEFAULT_MIN_SIZE
);

// then
assertThat(mergedSchedules).isEmpty();
Expand All @@ -43,7 +52,9 @@ void mergeContinuousTest() {
);

// when
List<CandidateSchedule> mergedSchedules = CandidateSchedule.mergeContinuous(schedules, this::isContinuous);
List<CandidateSchedule> mergedSchedules = CandidateSchedule.mergeContinuous(
schedules, this::isContinuous, DEFAULT_MIN_SIZE
);

// then
assertAll(
Expand All @@ -59,6 +70,95 @@ void mergeContinuousTest() {
);
}

@DisplayName("연속된 시간 길이가 주어진 길이보다 같거나 큰 경우만 병합한다.")
@ParameterizedTest
@CsvSource(value = {"0,9", "1,9", "2,6", "3,4", "4,2", "5,1"})
void mergeContinuousTestWhenHasMinSize(int minSize, int expected) {
// given
LocalDate today = LocalDate.now();
AttendeeGroup group = AttendeeGroupFixture.JAZZ_DAON_BAKEY.create();
List<CandidateSchedule> schedules = List.of(
// 30분 간격 시간 후보 3개
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0000),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0100),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0200),
// 60분 간격 시간 후보 2개
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0300),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0330),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0500),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0530),
// 90분 간격 시간 후보 2개
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1000),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1030),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1100),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1200),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1230),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1300),
// 120분 간격 시간 후보 1개
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1700),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1730),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1800),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1830),
// 150분 간격 시간 후보 1개
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_2030),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_2100),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_2130),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_2200),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_2230)
);

// when
List<CandidateSchedule> mergedSchedules = CandidateSchedule.mergeContinuous(
schedules, this::isContinuous, minSize
);

// then
assertThat(mergedSchedules).hasSize(expected);
}

@DisplayName("최소 시간이 최소 크기보다 작으면 예외가 발생한다.")
@Test
void mergeContinuousTestWhenHasMinSizeLessThan() {
// given
int givenMinSize = -1;
LocalDate today = LocalDate.now();
AttendeeGroup group = AttendeeGroupFixture.JAZZ_DAON_BAKEY.create();
List<CandidateSchedule> schedules = List.of(
// 30분 간격 시간 후보 3개
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0000),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0100),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0200),
// 60분 간격 시간 후보 2개
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0300),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0330),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0500),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_0530),
// 90분 간격 시간 후보 2개
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1000),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1030),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1100),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1200),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1230),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1300),
// 120분 간격 시간 후보 1개
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1700),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1730),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1800),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_1830),
// 150분 간격 시간 후보 1개
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_2030),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_2100),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_2130),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_2200),
createDiscreteCandidateSchedule(group, today, Timeslot.TIME_2230)
);

// when
assertThatThrownBy(() -> CandidateSchedule.mergeContinuous(schedules, this::isContinuous, givenMinSize))
.isInstanceOf(MomoException.class)
.hasMessage(ScheduleErrorCode.INVALID_MIN_TIME.message());
}

@DisplayName("자정을 포함하여 연속되는 시간의 경우 종료일자는 마지막 시간의 종료일자이다.")
@Test
void mergeContinuousIncludeMidnightTest() {
Expand All @@ -75,7 +175,9 @@ void mergeContinuousIncludeMidnightTest() {
);

// when
List<CandidateSchedule> mergedSchedules = CandidateSchedule.mergeContinuous(schedules, this::isContinuous);
List<CandidateSchedule> mergedSchedules = CandidateSchedule.mergeContinuous(
schedules, this::isContinuous, DEFAULT_MIN_SIZE
);

// then
assertAll(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -262,7 +262,7 @@ void recommendLongTermSchedules() {
scheduleRepository.saveAll(schedules);

RecommendedSchedulesResponse responses = scheduleService.recommendSchedules(
movieMeeting.getUuid(), LONG_TERM_ORDER.getType(), List.of(jazz.name(), daon.name())
movieMeeting.getUuid(), LONG_TERM_ORDER.getType(), List.of(jazz.name(), daon.name()), 0
);

assertThat(responses.recommendedSchedules()).containsExactly(
Expand Down Expand Up @@ -302,7 +302,7 @@ void recommendFastestSchedules() {
scheduleRepository.saveAll(schedules);

RecommendedSchedulesResponse responses = scheduleService.recommendSchedules(
movieMeeting.getUuid(), EARLIEST_ORDER.getType(), List.of(jazz.name(), daon.name())
movieMeeting.getUuid(), EARLIEST_ORDER.getType(), List.of(jazz.name(), daon.name()), 0
);

assertThat(responses.recommendedSchedules()).containsExactly(
Expand Down Expand Up @@ -399,7 +399,7 @@ void recommendContinuousSchedule() {
scheduleRepository.saveAll(schedules);

RecommendedSchedulesResponse responses = scheduleService.recommendSchedules(
movieMeeting.getUuid(), LONG_TERM_ORDER.getType(), List.of(jazz.name(), daon.name())
movieMeeting.getUuid(), LONG_TERM_ORDER.getType(), List.of(jazz.name(), daon.name()), 0
);

assertThat(responses.recommendedSchedules()).containsExactly(
Expand Down
Loading

0 comments on commit d7e05e8

Please sign in to comment.