Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
Expand Up @@ -5,13 +5,15 @@
import com.recyclestudy.member.controller.request.MemberNotificationTimeUpdateRequest;
import com.recyclestudy.member.controller.request.MemberSaveRequest;
import com.recyclestudy.member.controller.response.MemberFindResponse;
import com.recyclestudy.member.controller.response.MemberNotificationTimeFindResponse;
import com.recyclestudy.member.controller.response.MemberSaveResponse;
import com.recyclestudy.member.domain.DeviceIdentifier;
import com.recyclestudy.member.service.MemberService;
import com.recyclestudy.member.service.input.MemberFindInput;
import com.recyclestudy.member.service.input.MemberNotificationTimeUpdateInput;
import com.recyclestudy.member.service.input.MemberSaveInput;
import com.recyclestudy.member.service.output.MemberFindOutput;
import com.recyclestudy.member.service.output.MemberNotificationTimeFindOutput;
import com.recyclestudy.member.service.output.MemberSaveOutput;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
Expand Down Expand Up @@ -55,6 +57,15 @@ public ResponseEntity<MemberFindResponse> findAllMemberDevices(
return ResponseEntity.ok(response);
}

@GetMapping("/notification-time")
public ResponseEntity<MemberNotificationTimeFindResponse> findNotificationTime(
@AuthDevice final DeviceIdentifier identifier
) {
final MemberNotificationTimeFindOutput output = memberService.findNotificationTime(identifier);
final MemberNotificationTimeFindResponse response = MemberNotificationTimeFindResponse.from(output);
return ResponseEntity.ok(response);
}

@PatchMapping("/notification-time")
public ResponseEntity<Void> updateNotificationTime(
@RequestBody final MemberNotificationTimeUpdateRequest request,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,17 +2,16 @@

import com.recyclestudy.member.service.output.MemberFindOutput;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;

public record MemberFindResponse(String email, LocalTime notificationTime, List<MemberFindElement> devices) {
public record MemberFindResponse(String email, List<MemberFindElement> devices) {

public static MemberFindResponse from(final MemberFindOutput output) {
final List<MemberFindElement> memberFindElements = output.elements().stream()
.map(outputElement -> new MemberFindElement(outputElement.identifier().getValue(),
outputElement.createdAt()))
.toList();
return new MemberFindResponse(output.email().getValue(), output.notificationTime(), memberFindElements);
return new MemberFindResponse(output.email().getValue(), memberFindElements);
}

private record MemberFindElement(String identifier, LocalDateTime createdAt) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.recyclestudy.member.controller.response;

import com.recyclestudy.member.service.output.MemberNotificationTimeFindOutput;
import java.time.LocalTime;

public record MemberNotificationTimeFindResponse(LocalTime notificationTime) {
public static MemberNotificationTimeFindResponse from(final MemberNotificationTimeFindOutput output) {
return new MemberNotificationTimeFindResponse(output.notificationTime());
}
}
18 changes: 7 additions & 11 deletions src/main/java/com/recyclestudy/member/service/MemberService.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.recyclestudy.member.service.input.MemberNotificationTimeUpdateInput;
import com.recyclestudy.member.service.input.MemberSaveInput;
import com.recyclestudy.member.service.output.MemberFindOutput;
import com.recyclestudy.member.service.output.MemberNotificationTimeFindOutput;
import com.recyclestudy.member.service.output.MemberSaveOutput;
import java.time.Clock;
import java.time.LocalDateTime;
Expand All @@ -23,7 +24,6 @@
import java.util.Optional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.lang.Nullable;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand Down Expand Up @@ -54,18 +54,14 @@ public MemberSaveOutput saveDevice(final MemberSaveInput input) {
@Transactional(readOnly = true)
public MemberFindOutput findAllMemberDevices(final MemberFindInput input) {
final List<Device> devices = deviceRepository.findAllByMemberEmail(input.email());
final LocalTime notificationTime = findNotificationTime(input.email(), devices);
return MemberFindOutput.of(input.email(), notificationTime, devices);
return MemberFindOutput.of(input.email(), devices);
}

@Nullable
private LocalTime findNotificationTime(final Email email, final List<Device> devices) {
if (devices.isEmpty()) {
return memberRepository.findByEmail(email)
.map(Member::getNotificationTime)
.orElse(null);
}
return devices.getFirst().getMember().getNotificationTime();
@Transactional(readOnly = true)
public MemberNotificationTimeFindOutput findNotificationTime(final DeviceIdentifier identifier) {
final Member member = memberRepository.findByIdentifier(identifier)
.orElseThrow(() -> new UnauthorizedException("유효하지 않은 디바이스입니다"));
return MemberNotificationTimeFindOutput.from(member.getNotificationTime());
}

@Transactional
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,18 @@
import com.recyclestudy.member.domain.DeviceIdentifier;
import com.recyclestudy.member.domain.Email;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.List;

public record MemberFindOutput(Email email, LocalTime notificationTime, List<MemberFindElement> elements) {
public record MemberFindOutput(Email email, List<MemberFindElement> elements) {

public static MemberFindOutput of(
final Email email,
final LocalTime notificationTime,
final List<Device> devices
) {
final List<MemberFindElement> memberFindElements = devices.stream()
.map(device -> new MemberFindElement(device.getIdentifier(), device.getCreatedAt()))
.toList();
return new MemberFindOutput(email, notificationTime, memberFindElements);
return new MemberFindOutput(email, memberFindElements);
}

public record MemberFindElement(DeviceIdentifier identifier, LocalDateTime createdAt) {
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.recyclestudy.member.service.output;

import java.time.LocalTime;

public record MemberNotificationTimeFindOutput(LocalTime notificationTime) {
public static MemberNotificationTimeFindOutput from(final LocalTime notificationTime) {
return new MemberNotificationTimeFindOutput(notificationTime);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -14,13 +14,15 @@
import com.recyclestudy.member.service.MemberService;
import com.recyclestudy.member.service.input.MemberNotificationTimeUpdateInput;
import com.recyclestudy.member.service.output.MemberFindOutput;
import com.recyclestudy.member.service.output.MemberNotificationTimeFindOutput;
import com.recyclestudy.member.service.output.MemberSaveOutput;
import com.recyclestudy.restdocs.APIBaseTest;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.temporal.ChronoUnit;
import java.util.List;
import java.util.Optional;
import org.hamcrest.Matchers;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -121,7 +123,6 @@ void findAllMemberDevices() {

final MemberFindOutput output = new MemberFindOutput(
Email.from(email),
LocalTime.of(9, 0),
List.of(device1, device2)
);

Expand All @@ -143,8 +144,6 @@ void findAllMemberDevices() {
)
.responseFields(
fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"),
fieldWithPath("notificationTime").type(JsonFieldType.STRING)
.description("알림 시간").optional(),
fieldWithPath("devices").type(JsonFieldType.ARRAY).description("디바이스 목록"),
fieldWithPath("devices[].identifier").type(JsonFieldType.STRING)
.description("디바이스 식별자 값"),
Expand Down Expand Up @@ -428,7 +427,6 @@ void findAllMemberDevices_WithHeader() {

final MemberFindOutput output = new MemberFindOutput(
Email.from(email),
null,
List.of(device1, device2)
);

Expand All @@ -450,8 +448,6 @@ void findAllMemberDevices_WithHeader() {
)
.responseFields(
fieldWithPath("email").type(JsonFieldType.STRING).description("이메일"),
fieldWithPath("notificationTime").type(JsonFieldType.STRING)
.description("알림 시간").optional(),
fieldWithPath("devices").type(JsonFieldType.ARRAY).description("디바이스 목록"),
fieldWithPath("devices[].identifier").type(JsonFieldType.STRING)
.description("디바이스 식별자 값"),
Expand All @@ -471,6 +467,105 @@ void findAllMemberDevices_WithHeader() {
.body("devices", hasSize(2));
}

@Test
@DisplayName("멤버의 알림 시간을 조회한다")
void findNotificationTime() {
// given
final String headerIdentifier = "device-id-1";
final LocalTime notificationTime = LocalTime.of(9, 0);
final MemberNotificationTimeFindOutput output = new MemberNotificationTimeFindOutput(notificationTime);

given(memberService.findNotificationTime(any(DeviceIdentifier.class))).willReturn(output);

// when
// then
given(this.spec)
.filter(document(DEFAULT_REST_DOC_PATH,
builder()
.tag("Member")
.summary("멤버 알림 시간 조회")
.description("멤버의 알림 시간을 조회한다")
.requestHeaders(
headerWithName("X-Device-Id").description("디바이스 식별자")
)
.responseFields(
fieldWithPath("notificationTime").type(JsonFieldType.STRING)
.description("알림 시간 (HH:mm:ss)")
)
))
.header("X-Device-Id", headerIdentifier)
.when()
.get("/api/v1/members/notification-time")
.then()
.statusCode(HttpStatus.OK.value())
.body("notificationTime", equalTo("09:00:00"));
}

@Test
@DisplayName("알림 시간을 설정하지 않은 멤버 조회 시 null을 반환한다")
void findNotificationTime_NullNotificationTime() {
// given
final String headerIdentifier = "device-id-1";
final MemberNotificationTimeFindOutput output = new MemberNotificationTimeFindOutput(null);

given(memberService.findNotificationTime(any(DeviceIdentifier.class))).willReturn(output);

// when
// then
given(this.spec)
.filter(document(DEFAULT_REST_DOC_PATH,
builder()
.tag("Member")
.summary("멤버 알림 시간 조회")
.description("알림 시간을 설정하지 않은 멤버 조회 시 null을 반환한다")
.requestHeaders(
headerWithName("X-Device-Id").description("디바이스 식별자")
)
.responseFields(
fieldWithPath("notificationTime").type(JsonFieldType.STRING)
.description("알림 시간 (미설정 시 null)").optional()
)
))
.header("X-Device-Id", headerIdentifier)
.when()
.get("/api/v1/members/notification-time")
.then()
.statusCode(HttpStatus.OK.value())
.body("notificationTime", Matchers.nullValue());
}

@Test
@DisplayName("유효하지 않은 디바이스로 알림 시간 조회 시 401 응답을 반환한다")
void findNotificationTime_UnauthorizedDevice() {
// given
final String headerIdentifier = "invalid-device-id";

given(memberService.findNotificationTime(any(DeviceIdentifier.class)))
.willThrow(new UnauthorizedException("유효하지 않은 디바이스입니다"));

// when
// then
given(this.spec)
.filter(document(DEFAULT_REST_DOC_PATH,
builder()
.tag("Member")
.summary("멤버 알림 시간 조회")
.description("유효하지 않은 디바이스로 알림 시간 조회 시 401 응답을 반환한다")
.requestHeaders(
headerWithName("X-Device-Id").description("디바이스 식별자")
)
.responseFields(
fieldWithPath("message").type(JsonFieldType.STRING).description("에러 메시지")
)
))
.header("X-Device-Id", headerIdentifier)
.when()
.get("/api/v1/members/notification-time")
.then()
.statusCode(HttpStatus.UNAUTHORIZED.value())
.body("message", equalTo("유효하지 않은 디바이스입니다"));
}

@Test
@DisplayName("멤버의 알림 시간을 업데이트한다")
void updateNotificationTime() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import com.recyclestudy.member.service.input.MemberNotificationTimeUpdateInput;
import com.recyclestudy.member.service.input.MemberSaveInput;
import com.recyclestudy.member.service.output.MemberFindOutput;
import com.recyclestudy.member.service.output.MemberNotificationTimeFindOutput;
import com.recyclestudy.member.service.output.MemberSaveOutput;
import java.time.Clock;
import java.time.Instant;
Expand Down Expand Up @@ -241,6 +242,55 @@ void deleteDevice() {
verify(deviceRepository).deleteByIdentifier(targetDeviceIdentifier);
}

@Test
@DisplayName("디바이스 식별자로 멤버의 알림 시간을 조회한다")
void findNotificationTime() {
// given
final DeviceIdentifier identifier = DeviceIdentifier.from("device-id");
final Member member = Member.withoutId(Email.from("test@test.com"));
final LocalTime expectedTime = LocalTime.of(9, 0);
member.updateNotificationTime(expectedTime);

given(memberRepository.findByIdentifier(identifier)).willReturn(Optional.of(member));

// when
final MemberNotificationTimeFindOutput actual = memberService.findNotificationTime(identifier);

// then
assertThat(actual.notificationTime()).isEqualTo(expectedTime);
}

@Test
@DisplayName("알림 시간을 설정하지 않은 멤버 조회 시 null을 반환한다")
void findNotificationTime_NullNotificationTime() {
// given
final DeviceIdentifier identifier = DeviceIdentifier.from("device-id");
final Member member = Member.withoutId(Email.from("test@test.com"));

given(memberRepository.findByIdentifier(identifier)).willReturn(Optional.of(member));

// when
final MemberNotificationTimeFindOutput actual = memberService.findNotificationTime(identifier);

// then
assertThat(actual.notificationTime()).isNull();
}

@Test
@DisplayName("유효하지 않은 디바이스로 알림 시간 조회 시 예외를 던진다")
void findNotificationTime_UnauthorizedDevice() {
// given
final DeviceIdentifier identifier = DeviceIdentifier.from("invalid-id");

given(memberRepository.findByIdentifier(identifier)).willReturn(Optional.empty());

// when
// then
assertThatThrownBy(() -> memberService.findNotificationTime(identifier))
.isInstanceOf(UnauthorizedException.class)
.hasMessage("유효하지 않은 디바이스입니다");
}

@Test
@DisplayName("멤버의 알림 시간을 업데이트할 수 있다")
void updateNotificationTime() {
Expand Down