diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/album/controller/AlbumController.java b/cherrypic-api/src/main/java/org/cherrypic/domain/album/controller/AlbumController.java index 3b19183d..03ae65e4 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/album/controller/AlbumController.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/album/controller/AlbumController.java @@ -43,7 +43,7 @@ public AlbumUpdateResponse albumUpdate( } @PatchMapping("/{albumId}/permission") - @Operation(summary = "앨범 권한 부여 토글 상태 변경", description = "앨범의 권한 부여 토글 상태를 변경합니다.") + @Operation(summary = "앨범 권한 부여 상태 변경", description = "앨범의 권한 부여 허용 여부를 변경합니다.") public PermissionToggleResponse permissionToggle(@PathVariable Long albumId) { return albumService.togglePermission(albumId); } diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/album/dto/response/PermissionToggleResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/album/dto/response/PermissionToggleResponse.java index 1d9c1f63..aacf2802 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/album/dto/response/PermissionToggleResponse.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/album/dto/response/PermissionToggleResponse.java @@ -4,7 +4,7 @@ import org.cherrypic.album.entity.Album; public record PermissionToggleResponse( - @Schema(description = "권한 부여 토글 상태", example = "true") Boolean permissionControl) { + @Schema(description = "권한 부여 허용 여부", example = "true") Boolean permissionControl) { public static PermissionToggleResponse from(Album album) { return new PermissionToggleResponse(album.getPermissionControl()); } diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/member/controller/MemberController.java b/cherrypic-api/src/main/java/org/cherrypic/domain/member/controller/MemberController.java index 1103d8ea..ceec255b 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/member/controller/MemberController.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/member/controller/MemberController.java @@ -6,8 +6,10 @@ import lombok.RequiredArgsConstructor; import org.cherrypic.domain.member.dto.request.FcmTokenSaveRequest; import org.cherrypic.domain.member.dto.request.MemberProfileUpdateRequest; +import org.cherrypic.domain.member.dto.response.MarketingAgreeToggleResponse; import org.cherrypic.domain.member.dto.response.MemberInfoResponse; import org.cherrypic.domain.member.dto.response.MemberProfileUpdateResponse; +import org.cherrypic.domain.member.dto.response.ServiceAlarmAgreeToggleResponse; import org.cherrypic.domain.member.service.MemberService; import org.springframework.http.ResponseEntity; import org.springframework.web.bind.annotation.*; @@ -42,4 +44,16 @@ public ResponseEntity memberFcmTokenSave( memberService.saveFcmToken(request); return ResponseEntity.noContent().build(); } + + @PatchMapping("/me/service-alarm") + @Operation(summary = "서비스 알림 수신 동의 상태 변경", description = "앱 내 주요 서비스 관련 알림 수신 여부를 변경합니다.") + public ServiceAlarmAgreeToggleResponse serviceAlarmAgreeToggle() { + return memberService.toggleServiceAlarmAgree(); + } + + @PatchMapping("/me/marketing") + @Operation(summary = "마케팅 수신 동의 상태 변경", description = "회원의 마케팅 수신 여부를 변경합니다.") + public MarketingAgreeToggleResponse marketingAgreeToggle() { + return memberService.toggleMarketingAgree(); + } } diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/member/dto/response/MarketingAgreeToggleResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/member/dto/response/MarketingAgreeToggleResponse.java new file mode 100644 index 00000000..cf430beb --- /dev/null +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/member/dto/response/MarketingAgreeToggleResponse.java @@ -0,0 +1,11 @@ +package org.cherrypic.domain.member.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.cherrypic.member.entity.Member; + +public record MarketingAgreeToggleResponse( + @Schema(description = "마케팅 수신 동의 여부", example = "false") Boolean marketingAgree) { + public static MarketingAgreeToggleResponse from(Member member) { + return new MarketingAgreeToggleResponse(member.getMarketingAgree()); + } +} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/member/dto/response/MemberInfoResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/member/dto/response/MemberInfoResponse.java index 8f998514..c07ad966 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/member/dto/response/MemberInfoResponse.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/member/dto/response/MemberInfoResponse.java @@ -16,7 +16,9 @@ public record MemberInfoResponse( "http://k.kakaocdn.net/dn/ceTrU6/btsL0V0mhKO/DGqAZKAK/img_110x110.jpg") String profileImageUrl, @Schema(description = "회원 상태", example = "NORMAL") MemberStatus status, - @Schema(description = "회원 역할", example = "ROLE_USER") MemberRole role) { + @Schema(description = "회원 역할", example = "ROLE_USER") MemberRole role, + @Schema(description = "서비스 알림 수신 동의 여부", example = "false") Boolean serviceAlarmAgree, + @Schema(description = "마케팅 수신 동의 여부", example = "false") Boolean marketingAgree) { public static MemberInfoResponse from(Member member) { return new MemberInfoResponse( member.getId(), @@ -24,6 +26,8 @@ public static MemberInfoResponse from(Member member) { member.getNickname(), member.getProfileImageUrl(), member.getStatus(), - member.getRole()); + member.getRole(), + member.getServiceAlarmAgree(), + member.getMarketingAgree()); } } diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/member/dto/response/ServiceAlarmAgreeToggleResponse.java b/cherrypic-api/src/main/java/org/cherrypic/domain/member/dto/response/ServiceAlarmAgreeToggleResponse.java new file mode 100644 index 00000000..e69700f2 --- /dev/null +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/member/dto/response/ServiceAlarmAgreeToggleResponse.java @@ -0,0 +1,11 @@ +package org.cherrypic.domain.member.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; +import org.cherrypic.member.entity.Member; + +public record ServiceAlarmAgreeToggleResponse( + @Schema(description = "서비스 알림 수신 동의 여부", example = "false") Boolean serviceAlarmAgree) { + public static ServiceAlarmAgreeToggleResponse from(Member member) { + return new ServiceAlarmAgreeToggleResponse(member.getServiceAlarmAgree()); + } +} diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/member/repository/MemberRepository.java b/cherrypic-api/src/main/java/org/cherrypic/domain/member/repository/MemberRepository.java index f3dec136..5d53fb94 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/member/repository/MemberRepository.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/member/repository/MemberRepository.java @@ -1,10 +1,15 @@ package org.cherrypic.domain.member.repository; +import java.util.List; import java.util.Optional; import org.cherrypic.member.entity.Member; import org.cherrypic.member.entity.OauthInfo; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; public interface MemberRepository extends JpaRepository { Optional findByOauthInfo(OauthInfo oauthInfo); + + @Query("SELECT m.id FROM Member m WHERE m.id IN :ids AND m.serviceAlarmAgree = true") + List findServiceAlarmAgreedIds(List ids); } diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/member/service/MemberService.java b/cherrypic-api/src/main/java/org/cherrypic/domain/member/service/MemberService.java index cf1485c0..a22a3f18 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/member/service/MemberService.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/member/service/MemberService.java @@ -2,8 +2,10 @@ import org.cherrypic.domain.member.dto.request.FcmTokenSaveRequest; import org.cherrypic.domain.member.dto.request.MemberProfileUpdateRequest; +import org.cherrypic.domain.member.dto.response.MarketingAgreeToggleResponse; import org.cherrypic.domain.member.dto.response.MemberInfoResponse; import org.cherrypic.domain.member.dto.response.MemberProfileUpdateResponse; +import org.cherrypic.domain.member.dto.response.ServiceAlarmAgreeToggleResponse; public interface MemberService { MemberInfoResponse getMemberInfo(); @@ -11,4 +13,8 @@ public interface MemberService { MemberProfileUpdateResponse updateProfile(MemberProfileUpdateRequest request); void saveFcmToken(FcmTokenSaveRequest request); + + ServiceAlarmAgreeToggleResponse toggleServiceAlarmAgree(); + + MarketingAgreeToggleResponse toggleMarketingAgree(); } diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/member/service/MemberServiceImpl.java b/cherrypic-api/src/main/java/org/cherrypic/domain/member/service/MemberServiceImpl.java index b02128e6..ab6f5976 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/member/service/MemberServiceImpl.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/member/service/MemberServiceImpl.java @@ -4,8 +4,10 @@ import org.cherrypic.domain.image.event.ImageDeleteEvent; import org.cherrypic.domain.member.dto.request.FcmTokenSaveRequest; import org.cherrypic.domain.member.dto.request.MemberProfileUpdateRequest; +import org.cherrypic.domain.member.dto.response.MarketingAgreeToggleResponse; import org.cherrypic.domain.member.dto.response.MemberInfoResponse; import org.cherrypic.domain.member.dto.response.MemberProfileUpdateResponse; +import org.cherrypic.domain.member.dto.response.ServiceAlarmAgreeToggleResponse; import org.cherrypic.domain.notification.service.FcmTokenService; import org.cherrypic.global.util.MemberUtil; import org.cherrypic.member.entity.Member; @@ -50,4 +52,18 @@ public void saveFcmToken(FcmTokenSaveRequest request) { final Member currentMember = memberUtil.getCurrentMember(); fcmTokenService.saveFcmToken(currentMember.getId(), request.fcmToken()); } + + @Override + public ServiceAlarmAgreeToggleResponse toggleServiceAlarmAgree() { + final Member currentMember = memberUtil.getCurrentMember(); + currentMember.toggleServiceAlarmAgree(); + return ServiceAlarmAgreeToggleResponse.from(currentMember); + } + + @Override + public MarketingAgreeToggleResponse toggleMarketingAgree() { + final Member currentMember = memberUtil.getCurrentMember(); + currentMember.toggleMarketingAgree(); + return MarketingAgreeToggleResponse.from(currentMember); + } } diff --git a/cherrypic-api/src/main/java/org/cherrypic/domain/notification/service/NotificationServiceImpl.java b/cherrypic-api/src/main/java/org/cherrypic/domain/notification/service/NotificationServiceImpl.java index 3c5492be..e3d9654c 100644 --- a/cherrypic-api/src/main/java/org/cherrypic/domain/notification/service/NotificationServiceImpl.java +++ b/cherrypic-api/src/main/java/org/cherrypic/domain/notification/service/NotificationServiceImpl.java @@ -2,7 +2,6 @@ import java.util.List; import lombok.RequiredArgsConstructor; -import org.cherrypic.domain.album.repository.AlbumRepository; import org.cherrypic.domain.member.repository.MemberRepository; import org.cherrypic.domain.notification.repository.NotificationRepository; import org.springframework.stereotype.Service; @@ -21,7 +20,6 @@ public class NotificationServiceImpl implements NotificationService { private final FcmTokenService fcmTokenService; private final MemberRepository memberRepository; - private final AlbumRepository albumRepository; private final NotificationRepository notificationRepository; @Override @@ -36,7 +34,9 @@ public void sendAlbumDeleteNotification( notificationRepository.bulkInsertAlbumDeleteNotifications( albumId, senderId, PUSH_ALBUM_DELETE_TITLE, content); - List tokens = fcmTokenService.getFcmTokens(receiverIds); + List serviceAlarmAgreedIds = memberRepository.findServiceAlarmAgreedIds(receiverIds); + + List tokens = fcmTokenService.getFcmTokens(serviceAlarmAgreedIds); if (tokens.isEmpty()) { return; } diff --git a/cherrypic-api/src/test/java/org/cherrypic/album/controller/AlbumControllerTest.java b/cherrypic-api/src/test/java/org/cherrypic/album/controller/AlbumControllerTest.java index cabd844b..c45d2e35 100644 --- a/cherrypic-api/src/test/java/org/cherrypic/album/controller/AlbumControllerTest.java +++ b/cherrypic-api/src/test/java/org/cherrypic/album/controller/AlbumControllerTest.java @@ -579,7 +579,7 @@ class 앨범_수정_요청_시 { } @Nested - class 앨범_권한_부여_토글_상태_변경_요청_시 { + class 멤버별_권한_부여_상태_변경_요청_시 { @Test void 유효한_요청이면_권한_부여_토글_상태를_반환한다() throws Exception { diff --git a/cherrypic-api/src/test/java/org/cherrypic/album/service/AlbumServiceTest.java b/cherrypic-api/src/test/java/org/cherrypic/album/service/AlbumServiceTest.java index bb27b76f..852fc643 100644 --- a/cherrypic-api/src/test/java/org/cherrypic/album/service/AlbumServiceTest.java +++ b/cherrypic-api/src/test/java/org/cherrypic/album/service/AlbumServiceTest.java @@ -505,7 +505,7 @@ void setUp() { } @Nested - class 앨범_권한_부여_토글_상태를_변경_할_때 { + class 멤버별_권한_부여_상태를_변경할_때 { @BeforeEach void setUp() { @@ -572,7 +572,7 @@ void setUp() { } @Test - void 유효한_요청이면_앨범의_권한_부여_상태가_변경되고_LIMITED_참가자_권한이_STANDARD로_수정된다() { + void 유효한_요청이면_멤버별_권한_부여_상태를_변경하고_LIMITED_참가자들의_권한을_STANDARD로_변경한다() { // when albumService.togglePermission(1L); diff --git a/cherrypic-api/src/test/java/org/cherrypic/member/controller/MemberControllerTest.java b/cherrypic-api/src/test/java/org/cherrypic/member/controller/MemberControllerTest.java index c39ca47c..96d64dd6 100644 --- a/cherrypic-api/src/test/java/org/cherrypic/member/controller/MemberControllerTest.java +++ b/cherrypic-api/src/test/java/org/cherrypic/member/controller/MemberControllerTest.java @@ -9,8 +9,10 @@ import org.cherrypic.domain.member.controller.MemberController; import org.cherrypic.domain.member.dto.request.FcmTokenSaveRequest; import org.cherrypic.domain.member.dto.request.MemberProfileUpdateRequest; +import org.cherrypic.domain.member.dto.response.MarketingAgreeToggleResponse; import org.cherrypic.domain.member.dto.response.MemberInfoResponse; import org.cherrypic.domain.member.dto.response.MemberProfileUpdateResponse; +import org.cherrypic.domain.member.dto.response.ServiceAlarmAgreeToggleResponse; import org.cherrypic.domain.member.service.MemberService; import org.cherrypic.member.enums.MemberRole; import org.cherrypic.member.enums.MemberStatus; @@ -51,7 +53,9 @@ class 회원_정보_조회_요청_시 { "testNickname", "testProfileImageUrl", MemberStatus.NORMAL, - MemberRole.USER); + MemberRole.USER, + Boolean.FALSE, + Boolean.FALSE); given(memberService.getMemberInfo()).willReturn(response); @@ -66,7 +70,9 @@ class 회원_정보_조회_요청_시 { .andExpect(jsonPath("$.data.nickname").value("testNickname")) .andExpect(jsonPath("$.data.profileImageUrl").value("testProfileImageUrl")) .andExpect(jsonPath("$.data.status").value("NORMAL")) - .andExpect(jsonPath("$.data.role").value("USER")); + .andExpect(jsonPath("$.data.role").value("USER")) + .andExpect(jsonPath("$.data.serviceAlarmAgree").value("false")) + .andExpect(jsonPath("$.data.marketingAgree").value("false")); } } @@ -202,4 +208,45 @@ class FCM_토큰_저장_요청_시 { .andExpect(jsonPath("$.data.message").value("FCM Token은 비워둘 수 없습니다.")); } } + + @Nested + class 서비스_알림_수신_동의_상태_변경_요청_시 { + + @Test + void 유효한_요청이면_서비스_알림_수신_동의_상태를_변경하고_반환한다() throws Exception { + // given + ServiceAlarmAgreeToggleResponse response = + new ServiceAlarmAgreeToggleResponse(Boolean.TRUE); + + given(memberService.toggleServiceAlarmAgree()).willReturn(response); + + // when & then + ResultActions perform = mockMvc.perform(patch("/members/me/service-alarm")); + + perform.andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.data.serviceAlarmAgree").value("true")); + } + } + + @Nested + class 마케팅_수신_동의_상태_변경_요청_시 { + + @Test + void 유효한_요청이면_마케팅_수신_동의_상태를_변경하고_반환한다() throws Exception { + // given + MarketingAgreeToggleResponse response = new MarketingAgreeToggleResponse(Boolean.TRUE); + + given(memberService.toggleMarketingAgree()).willReturn(response); + + // when & then + ResultActions perform = mockMvc.perform(patch("/members/me/marketing")); + + perform.andExpect(status().isOk()) + .andExpect(jsonPath("$.success").value(true)) + .andExpect(jsonPath("$.status").value(200)) + .andExpect(jsonPath("$.data.marketingAgree").value("true")); + } + } } diff --git a/cherrypic-api/src/test/java/org/cherrypic/member/service/MemberServiceTest.java b/cherrypic-api/src/test/java/org/cherrypic/member/service/MemberServiceTest.java index 4f7b251c..937a2706 100644 --- a/cherrypic-api/src/test/java/org/cherrypic/member/service/MemberServiceTest.java +++ b/cherrypic-api/src/test/java/org/cherrypic/member/service/MemberServiceTest.java @@ -73,14 +73,18 @@ class 회원_정보를_조회할_때 { "nickname", "profileImageUrl", "role", - "status") + "status", + "serviceAlarmAgree", + "marketingAgree") .containsExactly( 1L, "testOauthProvider", "testNickname", "testProfileImageUrl", MemberRole.USER, - MemberStatus.NORMAL); + MemberStatus.NORMAL, + Boolean.FALSE, + Boolean.FALSE); } } @@ -155,4 +159,32 @@ void cleanUp() { assertThat(redisTemplate.opsForSet().size(("fcmToken:1"))).isEqualTo(2); } } + + @Nested + class 서비스_알림_수신_동의_상태를_변경할_때 { + + @Test + void 유효한_요청이면_서비스_알림_수신_동의_상태를_변경한다() { + // when + memberService.toggleServiceAlarmAgree(); + + // then + Member member = memberRepository.findById(1L).get(); + assertThat(member.getServiceAlarmAgree()).isTrue(); + } + } + + @Nested + class 마케팅_수신_동의_상태를_변경할_때 { + + @Test + void 유효한_요청이면_마케팅_수신_동의_상태를_변경한다() { + // when + memberService.toggleMarketingAgree(); + + // then + Member member = memberRepository.findById(1L).get(); + assertThat(member.getMarketingAgree()).isTrue(); + } + } } diff --git a/cherrypic-domain/src/main/java/org/cherrypic/member/entity/Member.java b/cherrypic-domain/src/main/java/org/cherrypic/member/entity/Member.java index 49593056..86de91b8 100644 --- a/cherrypic-domain/src/main/java/org/cherrypic/member/entity/Member.java +++ b/cherrypic-domain/src/main/java/org/cherrypic/member/entity/Member.java @@ -36,7 +36,9 @@ public class Member extends BaseTimeEntity { @Enumerated(EnumType.STRING) private MemberStatus status; - @NotNull private Boolean appAlarm = Boolean.FALSE; + @NotNull private Boolean serviceAlarmAgree; + + @NotNull private Boolean marketingAgree; @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) private List payments = new ArrayList<>(); @@ -56,12 +58,16 @@ private Member( String nickname, String profileImageUrl, MemberRole role, - MemberStatus status) { + MemberStatus status, + Boolean serviceAlarmAgree, + Boolean marketingAgree) { this.oauthInfo = oauthInfo; this.nickname = nickname; this.profileImageUrl = profileImageUrl; this.role = role; this.status = status; + this.serviceAlarmAgree = serviceAlarmAgree; + this.marketingAgree = marketingAgree; } public static Member createMember( @@ -72,6 +78,8 @@ public static Member createMember( .profileImageUrl(profileImageUrl) .role(MemberRole.USER) .status(MemberStatus.NORMAL) + .serviceAlarmAgree(Boolean.FALSE) + .marketingAgree(Boolean.FALSE) .build(); } @@ -79,4 +87,12 @@ public void updateMember(String nickname, String profileImageUrl) { this.nickname = nickname; this.profileImageUrl = profileImageUrl; } + + public void toggleServiceAlarmAgree() { + serviceAlarmAgree = !serviceAlarmAgree; + } + + public void toggleMarketingAgree() { + marketingAgree = !marketingAgree; + } } diff --git a/cherrypic-domain/src/main/resources/db/migration/V1__init.sql b/cherrypic-domain/src/main/resources/db/migration/V1__init.sql index 73707709..1d5258c0 100644 --- a/cherrypic-domain/src/main/resources/db/migration/V1__init.sql +++ b/cherrypic-domain/src/main/resources/db/migration/V1__init.sql @@ -6,7 +6,8 @@ CREATE TABLE member ( profile_image_url VARCHAR(255), role VARCHAR(255) NOT NULL CHECK(role IN ('ADMIN','USER')), status VARCHAR(255) NOT NULL CHECK(status IN ('NORMAL','DELETED','FORBIDDEN')), - app_alarm BOOLEAN NOT NULL, + service_alarm_agree BOOLEAN NOT NULL, + marketing_agree BOOLEAN NOT NULL, created_at DATETIME(6) NOT NULL, updated_at DATETIME(6) NOT NULL );