Skip to content

Commit

Permalink
[BE] 통합 캘린더 일정 조회 기능 구현 (#143)
Browse files Browse the repository at this point in the history
* feat: 여러 팀플래이스 아이디의 기간 내 일정들 조회 기능 구현

* feat: 사용자가 속한 모든팀의 기간 내 일정 조회 서비스 구현

* refactor: 중복코드 메서드 분리

* feat: 회원의 전체 일정 조회 api구현

* refactor: Param annotation 제거

* refactor: 일정조회 테스트 클래스 명 수정

* refactor: 캘린더 기간을 나타내는 객체 분리

- CalendarPeriod 레코드 생성
- 기간계산 로직 분리

* test: 누락된 테스트 어노테이션 제거
  • Loading branch information
pilyang authored Jul 23, 2023
1 parent 3919b6b commit 41adef9
Show file tree
Hide file tree
Showing 12 changed files with 263 additions and 25 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import team.teamby.teambyteam.member.domain.vo.Email;
import team.teamby.teambyteam.member.domain.vo.Name;
import team.teamby.teambyteam.member.domain.vo.ProfileImageUrl;
import team.teamby.teambyteam.teamplace.domain.TeamPlace;

import java.util.List;

Expand Down Expand Up @@ -45,4 +46,10 @@ public Member(
this.profileImageUrl = profileImageUrl;
this.memberTeamPlaces = memberTeamPlaces;
}

public List<TeamPlace> getTeamPlaces() {
return getMemberTeamPlaces().stream()
.map(MemberTeamPlace::getTeamPlace)
.toList();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,11 @@
import org.springframework.data.jpa.repository.JpaRepository;
import team.teamby.teambyteam.member.domain.vo.Email;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {

boolean existsByEmail(Email email);

Optional<Member> findByEmail(Email email);
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,32 +3,26 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import team.teamby.teambyteam.schedule.application.dto.ScheduleRegisterRequest;
import team.teamby.teambyteam.schedule.application.dto.ScheduleResponse;
import team.teamby.teambyteam.schedule.application.dto.SchedulesResponse;
import team.teamby.teambyteam.schedule.application.dto.ScheduleUpdateRequest;
import team.teamby.teambyteam.schedule.domain.Schedule;
import team.teamby.teambyteam.schedule.domain.ScheduleRepository;
import team.teamby.teambyteam.schedule.domain.Span;
import team.teamby.teambyteam.schedule.domain.Title;
import team.teamby.teambyteam.member.configuration.dto.MemberEmailDto;
import team.teamby.teambyteam.member.domain.Member;
import team.teamby.teambyteam.member.domain.MemberRepository;
import team.teamby.teambyteam.member.domain.vo.Email;
import team.teamby.teambyteam.member.exception.MemberException;
import team.teamby.teambyteam.schedule.application.dto.*;
import team.teamby.teambyteam.schedule.domain.*;
import team.teamby.teambyteam.schedule.exception.ScheduleException;
import team.teamby.teambyteam.teamplace.domain.TeamPlace;
import team.teamby.teambyteam.teamplace.domain.TeamPlaceRepository;
import team.teamby.teambyteam.teamplace.exception.TeamPlaceException;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;

@Service
@Transactional
@RequiredArgsConstructor
public class ScheduleService {

private static final int FIRST_DAY_OF_MONTH = 1;
private static final LocalTime START_TIME_OF_DAY = LocalTime.of(0, 0, 0);
public static final int NEXT_MONTH_OFFSET = 1;

private final MemberRepository memberRepository;
private final ScheduleRepository scheduleRepository;
private final TeamPlaceRepository teamPlaceRepository;

Expand Down Expand Up @@ -79,17 +73,34 @@ public SchedulesResponse findScheduleInPeriod(final Long teamPlaceId, final int
// TODO: 상의해보기 - 팀플레이스 소속 멤버 검증시 팀플레이스 아이디가 검증이 될 건데 해당 붑ㄴ에 대한 재 검증이 필요한가?
checkTeamPlaceExist(teamPlaceId);

final LocalDate startDate = LocalDate.of(targetYear, targetMonth, FIRST_DAY_OF_MONTH);
final LocalDate endDate = startDate.plusMonths(NEXT_MONTH_OFFSET).withDayOfMonth(FIRST_DAY_OF_MONTH);
final LocalDateTime startDateTime = LocalDateTime.of(startDate, START_TIME_OF_DAY);
final LocalDateTime endDateTime = LocalDateTime.of(endDate, START_TIME_OF_DAY);

final CalendarPeriod period = CalendarPeriod.of(targetYear, targetMonth);
final List<Schedule> schedules = scheduleRepository
.findAllByTeamPlaceIdAndPeriod(teamPlaceId, startDateTime, endDateTime);
.findAllByTeamPlaceIdAndPeriod(teamPlaceId, period.startDateTime(), period.endDatetime());

return SchedulesResponse.of(schedules);
}

@Transactional(readOnly = true)
public SchedulesWithTeamPlaceIdResponse findScheduleInPeriod(
final MemberEmailDto memberEmailDto,
final int targetYear,
final int targetMonth
) {
final Member member = memberRepository.findByEmail(new Email(memberEmailDto.email()))
.orElseThrow(() -> new MemberException.MemberNotFoundException("사용자를 찾을 수 없습니다."));

final List<Long> participatedTeamPlaceIds = member.getTeamPlaces()
.stream()
.map(TeamPlace::getId)
.toList();

final CalendarPeriod period = CalendarPeriod.of(targetYear, targetMonth);
final List<Schedule> schedules = scheduleRepository
.findAllByTeamPlaceIdAndPeriod(participatedTeamPlaceIds, period.startDateTime(), period.endDatetime());

return SchedulesWithTeamPlaceIdResponse.of(schedules);
}

public void update(final ScheduleUpdateRequest scheduleUpdateRequest, final Long teamPlaceId, final Long scheduleId) {
checkTeamPlaceExist(teamPlaceId);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package team.teamby.teambyteam.schedule.application.dto;

import team.teamby.teambyteam.schedule.domain.Schedule;
import team.teamby.teambyteam.schedule.domain.Span;

import java.time.format.DateTimeFormatter;

public record ScheduleWithTeamPlaceIdResponse(
Long id,
Long teamPlaceId,
String title,
String startDateTime,
String endDAteTime
) {
private static final String DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";

public static ScheduleWithTeamPlaceIdResponse of(final Schedule schedule) {
final Span span = schedule.getSpan();
final DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(DATE_TIME_FORMAT);
final String startDateTime = span.getStartDateTime().format(dateTimeFormatter);
final String endDateTime = span.getEndDateTime().format(dateTimeFormatter);

return new ScheduleWithTeamPlaceIdResponse(
schedule.getId(),
schedule.getTeamPlaceId(),
schedule.getTitle().getValue(),
startDateTime,
endDateTime
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package team.teamby.teambyteam.schedule.application.dto;

import team.teamby.teambyteam.schedule.domain.Schedule;

import java.util.List;

public record SchedulesWithTeamPlaceIdResponse(
List<ScheduleWithTeamPlaceIdResponse> schedules
) {
public static SchedulesWithTeamPlaceIdResponse of(final List<Schedule> schedules) {
return new SchedulesWithTeamPlaceIdResponse(schedules.stream()
.map(ScheduleWithTeamPlaceIdResponse::of)
.toList()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package team.teamby.teambyteam.schedule.domain;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;

public record CalendarPeriod(
LocalDateTime startDateTime,
LocalDateTime endDatetime
) {
private static final int FIRST_DAY_OF_MONTH = 1;
private static final LocalTime START_TIME_OF_DAY = LocalTime.of(0, 0, 0);
public static final int NEXT_MONTH_OFFSET = 1;

public static CalendarPeriod of(final int year, final int month) {
final LocalDate startDate = LocalDate.of(year, month, FIRST_DAY_OF_MONTH);
final LocalDate endDate = startDate.plusMonths(NEXT_MONTH_OFFSET).withDayOfMonth(FIRST_DAY_OF_MONTH);

return new CalendarPeriod(LocalDateTime.of(startDate, START_TIME_OF_DAY), LocalDateTime.of(endDate, START_TIME_OF_DAY));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -28,4 +28,23 @@ List<Schedule> findAllByTeamPlaceIdAndPeriod(
LocalDateTime lastDateTime
);

/**
* Find All schedules in range
*
* @param teamPlaceIds teamPlaceId for the schedule
* @param firstDateTime first-date-time of the period (Inclusive)
* @param lastDateTime last=date-time of the period (Exclusive)
* @return List of the Schedules. If there is no Schedule, it will return the List with size 0.
*/
@Query("SELECT s FROM Schedule s " +
"WHERE s.teamPlaceId IN :teamPlaceIds " +
"AND s.span.startDateTime < :lastDateTime " +
"AND s.span.endDateTime >= :firstDateTime " +
"ORDER BY s.span.startDateTime ASC"
)
List<Schedule> findAllByTeamPlaceIdAndPeriod(
List<Long> teamPlaceIds,
LocalDateTime firstDateTime,
LocalDateTime lastDateTime
);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package team.teamby.teambyteam.schedule.presentation;

import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import team.teamby.teambyteam.member.configuration.AuthPrincipal;
import team.teamby.teambyteam.member.configuration.dto.MemberEmailDto;
import team.teamby.teambyteam.schedule.application.ScheduleService;
import team.teamby.teambyteam.schedule.application.dto.SchedulesWithTeamPlaceIdResponse;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/my-calendar")
public class MyCalendarScheduleController {

private final ScheduleService scheduleService;

@GetMapping("/schedules")
public ResponseEntity<SchedulesWithTeamPlaceIdResponse> findSchedulesInPeriod(
@AuthPrincipal final MemberEmailDto memberEmailDto,
@RequestParam final Integer year,
@RequestParam final Integer month
) {
final SchedulesWithTeamPlaceIdResponse responseBody = scheduleService.findScheduleInPeriod(memberEmailDto, year, month);

return ResponseEntity.ok(responseBody);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,7 @@
import team.teamby.teambyteam.schedule.application.dto.ScheduleRegisterRequest;
import team.teamby.teambyteam.schedule.application.dto.ScheduleResponse;
import team.teamby.teambyteam.schedule.application.dto.ScheduleUpdateRequest;
import team.teamby.teambyteam.schedule.application.dto.ScheduleWithTeamPlaceIdResponse;

import java.time.LocalDateTime;
import java.util.HashMap;
Expand Down Expand Up @@ -64,6 +65,7 @@ public void getSpecificSchedule() {
});
}

@Test
@DisplayName("존재하지 않는 일정 조회시도시 실패한다.")
void failGetScheduleWhichIsNotExist() {
// given
Expand Down Expand Up @@ -426,6 +428,35 @@ void failNotExistScheduleId() {
}
}

@Nested
@DisplayName("개인 일정 조회를 한다")
class MyCalendarFindSchedule {

@Test
@DisplayName("기간으로 조회 성공한다.")
void success() {
// given
final int year = 2023;
final int month = 6;

// when
final ExtractableResponse<Response> response = requestMySchedulesInPeriod(year, month);
final List<ScheduleWithTeamPlaceIdResponse> schedules = response.jsonPath().getList("schedules", ScheduleWithTeamPlaceIdResponse.class);

//then
assertSoftly(softly -> {
softly.assertThat(response.statusCode()).isEqualTo(HttpStatus.OK.value());
softly.assertThat(schedules).hasSize(5);
softly.assertThat(schedules.get(0).title()).isEqualTo("3번 팀플 6월 첫날");
softly.assertThat(schedules.get(1).title()).isEqualTo("1번 팀플 6월 일정");
softly.assertThat(schedules.get(2).title()).isEqualTo("3번 팀플 A");
softly.assertThat(schedules.get(3).title()).isEqualTo("1번 팀플 장기 일정");
softly.assertThat(schedules.get(4).title()).isEqualTo("3번 팀플 B");
});
}

}

private ExtractableResponse<Response> registerScheduleRequest(final Long teamPlaceId, final ScheduleRegisterRequest request) {
return RestAssured.given().log().all()
.header("Authorization", JWT_PREFIX + JWT_TOKEN)
Expand Down Expand Up @@ -457,6 +488,17 @@ private ExtractableResponse<Response> requestTeamPlaceSchedulesInPeriod(final Lo
.extract();
}

private ExtractableResponse<Response> requestMySchedulesInPeriod(final Integer year, final Integer month) {
return RestAssured.given().log().all()
.header(new Header("Authorization", JWT_PREFIX + JWT_TOKEN))
.queryParam("year", year)
.queryParam("month", month)
.when().log().all()
.get("/api/my-calendar/schedules")
.then().log().all()
.extract();
}

private ExtractableResponse<Response> updateSchedule(final Long id, final Long teamPlaceId, final ScheduleUpdateRequest request) {
return RestAssured.given().log().all()
.header("Authorization", JWT_PREFIX + JWT_TOKEN)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,8 @@
import org.springframework.test.context.jdbc.Sql;
import org.springframework.transaction.annotation.Transactional;
import team.teamby.teambyteam.fixtures.ScheduleFixtures;
import team.teamby.teambyteam.schedule.application.dto.ScheduleRegisterRequest;
import team.teamby.teambyteam.schedule.application.dto.ScheduleResponse;
import team.teamby.teambyteam.schedule.application.dto.SchedulesResponse;
import team.teamby.teambyteam.schedule.application.dto.ScheduleUpdateRequest;
import team.teamby.teambyteam.member.configuration.dto.MemberEmailDto;
import team.teamby.teambyteam.schedule.application.dto.*;
import team.teamby.teambyteam.schedule.domain.Schedule;
import team.teamby.teambyteam.schedule.domain.ScheduleRepository;
import team.teamby.teambyteam.schedule.exception.ScheduleException;
Expand Down Expand Up @@ -146,6 +144,36 @@ void firstAndLastDateScheduleFind() {
}
}

@Nested
@DisplayName("통합 캘린더 정보 조회 시")
class FindSchedulesByUser {

@Test
@DisplayName("통합 캘린더 정보조회를 성공한다.")
void success() {
// given
// member who participate in team 2, 3
final MemberEmailDto memberEmailDto = new MemberEmailDto("[email protected]");
final int year = 2023;
final int month = 6;

// when
final SchedulesWithTeamPlaceIdResponse scheduleInPeriod = scheduleService.findScheduleInPeriod(memberEmailDto, year, month);
final List<ScheduleWithTeamPlaceIdResponse> scheduleResponses = scheduleInPeriod.schedules();

//then
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(scheduleResponses).hasSize(5);
softly.assertThat(scheduleResponses.get(0).title()).isEqualTo("3번 팀플 6월 첫날");
softly.assertThat(scheduleResponses.get(1).title()).isEqualTo("2번 팀플 6월 첫날");
softly.assertThat(scheduleResponses.get(2).title()).isEqualTo("3번 팀플 A");
softly.assertThat(scheduleResponses.get(3).title()).isEqualTo("2번 팀플 6월 어느날");
softly.assertThat(scheduleResponses.get(4).title()).isEqualTo("3번 팀플 B");
});
}

}

@Nested
@DisplayName("일정 등록 시")
class RegisterSchedule {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,28 @@ void findTeamPlaceScheduleInRange() {
});
}

@Test
@DisplayName("특정 기간 내 팀플레이스 일정을 조회한다.")
void findMultipleTeamPlaceScheduleInRange() {
// given
final List<Long> teamPlaceId = List.of(2L, 3L);
final LocalDateTime firstDateTime = LocalDateTime.of(2023, 6, 1, 0, 0);
final LocalDateTime endDateTime = LocalDateTime.of(2023, 7, 1, 0, 0);

// when
final List<Schedule> schedules = scheduleRepository.findAllByTeamPlaceIdAndPeriod(teamPlaceId, firstDateTime, endDateTime);

//then
SoftAssertions.assertSoftly(softly -> {
softly.assertThat(schedules).hasSize(5);
softly.assertThat(schedules.get(0).getTitle().getValue()).isEqualTo("3번 팀플 6월 첫날");
softly.assertThat(schedules.get(1).getTitle().getValue()).isEqualTo("2번 팀플 6월 첫날");
softly.assertThat(schedules.get(2).getTitle().getValue()).isEqualTo("3번 팀플 A");
softly.assertThat(schedules.get(3).getTitle().getValue()).isEqualTo("2번 팀플 6월 어느날");
softly.assertThat(schedules.get(4).getTitle().getValue()).isEqualTo("3번 팀플 B");
});
}

@ParameterizedTest
@CsvSource(value = {"-1:false", "1:true"}, delimiter = ':')
@DisplayName("일정이 존재하면 true, 존재하지 않으면 false를 반환한다.")
Expand Down
Loading

0 comments on commit 41adef9

Please sign in to comment.