From 42559665471fb249b96044da8aeb7964265723b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=AA=A8?= Date: Fri, 9 Feb 2024 16:18:46 +0900 Subject: [PATCH 1/4] =?UTF-8?q?feat:=20=ED=8C=94=EB=A1=9C=EC=9A=B0=20?= =?UTF-8?q?=EC=B7=A8=EC=86=8C=20=EC=8B=9C=20=EC=83=81=EB=8C=80=EC=9D=98=20?= =?UTF-8?q?=ED=8C=94=EB=A1=9C=EC=9A=B0=20=EC=83=81=ED=83=9C=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD=20(#2?= =?UTF-8?q?96)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 팔로우 취소 시 상대의 팔로우 상태 응답 하도록 변경 * test: 팔로우 취소 시 follow Status 테스트 코드 추가 --- .../domain/follow/api/FollowController.java | 5 +- .../follow/application/FollowService.java | 10 +++- .../follow/application/FollowServiceTest.java | 49 +++++++++++++++++++ 3 files changed, 61 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/depromeet/domain/follow/api/FollowController.java b/src/main/java/com/depromeet/domain/follow/api/FollowController.java index b85fe03bf..89818a70a 100644 --- a/src/main/java/com/depromeet/domain/follow/api/FollowController.java +++ b/src/main/java/com/depromeet/domain/follow/api/FollowController.java @@ -29,8 +29,9 @@ public ResponseEntity followCreate(@Valid @RequestBody FollowCreateRequest @DeleteMapping @Operation(summary = "팔로우 취소", description = "팔로우를 취소합니다.") - public void followDelete(@Valid @RequestBody FollowDeleteRequest request) { - followService.deleteFollow(request); + public ResponseEntity followDelete( + @Valid @RequestBody FollowDeleteRequest request) { + return ResponseEntity.ok(followService.deleteFollow(request)); } @GetMapping("/{targetId}") diff --git a/src/main/java/com/depromeet/domain/follow/application/FollowService.java b/src/main/java/com/depromeet/domain/follow/application/FollowService.java index 34c488f91..6278b5fca 100644 --- a/src/main/java/com/depromeet/domain/follow/application/FollowService.java +++ b/src/main/java/com/depromeet/domain/follow/application/FollowService.java @@ -62,7 +62,7 @@ public void createFollow(FollowCreateRequest request) { memberRelationRepository.save(memberRelation); } - public void deleteFollow(FollowDeleteRequest request) { + public FollowerDeletedResponse deleteFollow(FollowDeleteRequest request) { final Member currentMember = memberUtil.getCurrentMember(); Member targetMember = getTargetMember(request.targetId()); @@ -79,6 +79,14 @@ public void deleteFollow(FollowDeleteRequest request) { notificationRepository.delete(notification); } memberRelationRepository.delete(memberRelation); + + Optional optionalMemberRelation = + memberRelationRepository.findBySourceIdAndTargetId( + targetMember.getId(), currentMember.getId()); + + return optionalMemberRelation.isPresent() + ? FollowerDeletedResponse.from(FollowStatus.FOLLOWED_BY_ME) + : FollowerDeletedResponse.from(FollowStatus.NOT_FOLLOWING); } @Transactional(readOnly = true) diff --git a/src/test/java/com/depromeet/domain/follow/application/FollowServiceTest.java b/src/test/java/com/depromeet/domain/follow/application/FollowServiceTest.java index f5a05bafa..c64bbb0df 100644 --- a/src/test/java/com/depromeet/domain/follow/application/FollowServiceTest.java +++ b/src/test/java/com/depromeet/domain/follow/application/FollowServiceTest.java @@ -177,6 +177,55 @@ class 팔로우를_취소할_때 { .hasMessage(ErrorCode.FOLLOW_NOT_EXIST.getMessage()); } + @Test + void 상대가_나를_팔로우_하고_있다면_FOLLOW_STATUE가_FOLLOWED_BY_ME로_응답한다() { + Long targetId = 2L; + FollowDeleteRequest request = new FollowDeleteRequest(targetId); + Member currentMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("testNickname1", "testImageUrl1"))); + Member targetMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("testNickname2", "testImageUrl2"))); + MemberRelation memberRelation = + MemberRelation.createMemberRelation(currentMember, targetMember); + MemberRelation memberRelation2 = + MemberRelation.createMemberRelation(targetMember, currentMember); + memberRelationRepository.save(memberRelation); + memberRelationRepository.save(memberRelation2); + + // when + FollowerDeletedResponse response = followService.deleteFollow(request); + + // then + assertEquals(FollowStatus.FOLLOWED_BY_ME, response.followStatus()); + } + + @Test + void 상대가_나를_팔로우_하고_있지_않다면_FOLLOW_STATUE가_NOT_FOLLOWING로_응답한다() { + Long targetId = 2L; + FollowDeleteRequest request = new FollowDeleteRequest(targetId); + Member currentMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("testNickname1", "testImageUrl1"))); + Member targetMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("testNickname2", "testImageUrl2"))); + MemberRelation memberRelation = + MemberRelation.createMemberRelation(currentMember, targetMember); + memberRelationRepository.save(memberRelation); + + // when + FollowerDeletedResponse response = followService.deleteFollow(request); + + // then + assertEquals(FollowStatus.NOT_FOLLOWING, response.followStatus()); + } + @Test void 정상적이라면_팔로우가_취소된다() { Long targetId = 2L; From 76fb898983a907e62ae94a81c17539f93cf83596 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Fri, 9 Feb 2024 01:51:22 -0800 Subject: [PATCH 2/4] =?UTF-8?q?feat:=20Retension=20=EC=9D=B4=EB=B2=A4?= =?UTF-8?q?=ED=8A=B8=20=ED=91=B8=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20(#300)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 이벤트 11시 푸시 알림 * feat: 이벤트 푸시 알림 밤 10시로 변경 * feat: 쿼리 수정 * feat: 알림 제목, 내용 상수화 처리 --- .../constants/PushNotificationConstants.java | 12 ++++++ .../follow/application/FollowService.java | 5 +-- .../member/application/MemberService.java | 20 ++++++++++ .../domain/member/dao/MemberRepository.java | 2 +- .../member/dao/MemberRepositoryCustom.java | 9 +++++ .../member/dao/MemberRepositoryImpl.java | 40 +++++++++++++++++++ .../member/MemberBatchScheduler.java | 20 ++++++++++ 7 files changed, 104 insertions(+), 4 deletions(-) create mode 100644 src/main/java/com/depromeet/domain/common/constants/PushNotificationConstants.java create mode 100644 src/main/java/com/depromeet/domain/member/dao/MemberRepositoryCustom.java create mode 100644 src/main/java/com/depromeet/domain/member/dao/MemberRepositoryImpl.java create mode 100644 src/main/java/com/depromeet/scheduler/member/MemberBatchScheduler.java diff --git a/src/main/java/com/depromeet/domain/common/constants/PushNotificationConstants.java b/src/main/java/com/depromeet/domain/common/constants/PushNotificationConstants.java new file mode 100644 index 000000000..0f74ce743 --- /dev/null +++ b/src/main/java/com/depromeet/domain/common/constants/PushNotificationConstants.java @@ -0,0 +1,12 @@ +package com.depromeet.domain.common.constants; + +import lombok.AccessLevel; +import lombok.NoArgsConstructor; + +@NoArgsConstructor(access = AccessLevel.PRIVATE) +public class PushNotificationConstants { + public static final String PUSH_SERVICE_TITLE = "10MM"; + public static final String PUSH_SERVICE_CONTENT = "%s님이 회원님을 팔로우하기 시작했습니다🥳"; + public static final String PUSH_NON_COMPLETE_MISSION_SERVICE_CONTENT = + "아직 오늘 미션을 완료하지 않았어요! 10분 동안 빠르게 완료해볼까요?"; +} diff --git a/src/main/java/com/depromeet/domain/follow/application/FollowService.java b/src/main/java/com/depromeet/domain/follow/application/FollowService.java index 6278b5fca..85c22a86e 100644 --- a/src/main/java/com/depromeet/domain/follow/application/FollowService.java +++ b/src/main/java/com/depromeet/domain/follow/application/FollowService.java @@ -1,5 +1,7 @@ package com.depromeet.domain.follow.application; +import static com.depromeet.domain.common.constants.PushNotificationConstants.*; + import com.depromeet.domain.follow.dao.MemberRelationRepository; import com.depromeet.domain.follow.domain.MemberRelation; import com.depromeet.domain.follow.dto.request.FollowCreateRequest; @@ -34,9 +36,6 @@ public class FollowService { private final MemberRelationRepository memberRelationRepository; private final FcmService fcmService; - private static final String PUSH_SERVICE_TITLE = "10MM"; - private static final String PUSH_SERVICE_CONTENT = "%s님이 회원님을 팔로우하기 시작했습니다🥳"; - public void createFollow(FollowCreateRequest request) { final Member currentMember = memberUtil.getCurrentMember(); Member targetMember = getTargetMember(request.targetId()); diff --git a/src/main/java/com/depromeet/domain/member/application/MemberService.java b/src/main/java/com/depromeet/domain/member/application/MemberService.java index d94f9551c..98b6b9018 100644 --- a/src/main/java/com/depromeet/domain/member/application/MemberService.java +++ b/src/main/java/com/depromeet/domain/member/application/MemberService.java @@ -1,5 +1,7 @@ package com.depromeet.domain.member.application; +import static com.depromeet.domain.common.constants.PushNotificationConstants.*; + import com.depromeet.domain.auth.dao.RefreshTokenRepository; import com.depromeet.domain.auth.dto.request.UsernameCheckRequest; import com.depromeet.domain.follow.dao.MemberRelationRepository; @@ -14,9 +16,11 @@ import com.depromeet.domain.member.dto.response.MemberFindOneResponse; import com.depromeet.domain.member.dto.response.MemberSearchResponse; import com.depromeet.domain.member.dto.response.MemberSocialInfoResponse; +import com.depromeet.global.config.fcm.FcmService; import com.depromeet.global.error.exception.CustomException; import com.depromeet.global.error.exception.ErrorCode; import com.depromeet.global.util.MemberUtil; +import java.time.LocalDateTime; import java.util.ArrayList; import java.util.Comparator; import java.util.List; @@ -37,6 +41,7 @@ public class MemberService { private final RefreshTokenRepository refreshTokenRepository; private final MemberRelationRepository memberRelationRepository; private final MemberUtil memberUtil; + private final FcmService fcmService; @Transactional(readOnly = true) public MemberFindOneResponse findMemberInfo() { @@ -184,6 +189,21 @@ public void updateFcmToken(UpdateFcmTokenRequest updateFcmTokenRequest) { currentMember.updateFcmToken(currentMember.getFcmInfo(), updateFcmTokenRequest.fcmToken()); } + @Transactional(readOnly = true) + public void pushNotificationMissionRequest() { + LocalDateTime today = LocalDateTime.now(); + List nonMissionNonCompletedMembers = + memberRepository.findMissionNonCompletedMembers(today); + List tokenList = + nonMissionNonCompletedMembers.stream() + .map(member -> member.getFcmInfo().getFcmToken()) + .toList(); + if (!tokenList.isEmpty()) { + fcmService.sendGroupMessageAsync( + tokenList, PUSH_SERVICE_TITLE, PUSH_NON_COMPLETE_MISSION_SERVICE_CONTENT); + } + } + private String escapeSpecialCharacters(String nickname) { // 여기서 특수문자를 '_'로 대체할 수 있도록 정규표현식을 활용하여 구현 return nickname == null ? "" : nickname.replaceAll("[^0-9a-zA-Z가-힣 ]", "_"); diff --git a/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java b/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java index 186f9304e..e34f9ef30 100644 --- a/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java +++ b/src/main/java/com/depromeet/domain/member/dao/MemberRepository.java @@ -9,7 +9,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.data.jpa.repository.Query; -public interface MemberRepository extends JpaRepository { +public interface MemberRepository extends JpaRepository, MemberRepositoryCustom { Optional findByOauthInfoAndStatus(OauthInfo oauthInfo, MemberStatus status); diff --git a/src/main/java/com/depromeet/domain/member/dao/MemberRepositoryCustom.java b/src/main/java/com/depromeet/domain/member/dao/MemberRepositoryCustom.java new file mode 100644 index 000000000..9f1f4cc4a --- /dev/null +++ b/src/main/java/com/depromeet/domain/member/dao/MemberRepositoryCustom.java @@ -0,0 +1,9 @@ +package com.depromeet.domain.member.dao; + +import com.depromeet.domain.member.domain.Member; +import java.time.LocalDateTime; +import java.util.List; + +public interface MemberRepositoryCustom { + List findMissionNonCompletedMembers(LocalDateTime today); +} diff --git a/src/main/java/com/depromeet/domain/member/dao/MemberRepositoryImpl.java b/src/main/java/com/depromeet/domain/member/dao/MemberRepositoryImpl.java new file mode 100644 index 000000000..4db6e264a --- /dev/null +++ b/src/main/java/com/depromeet/domain/member/dao/MemberRepositoryImpl.java @@ -0,0 +1,40 @@ +package com.depromeet.domain.member.dao; + +import static com.depromeet.domain.member.domain.QMember.*; +import static com.depromeet.domain.mission.domain.QMission.*; +import static com.depromeet.domain.missionRecord.domain.QMissionRecord.*; + +import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.missionRecord.domain.ImageUploadStatus; +import com.querydsl.jpa.impl.JPAQueryFactory; +import java.time.LocalDateTime; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Repository; + +@Repository +@RequiredArgsConstructor +public class MemberRepositoryImpl implements MemberRepositoryCustom { + + private final JPAQueryFactory jpaQueryFactory; + + @Override + public List findMissionNonCompletedMembers(LocalDateTime today) { + LocalDateTime start = today.toLocalDate().atStartOfDay(); + LocalDateTime end = today.toLocalDate().atTime(23, 59, 59); + return jpaQueryFactory + .selectFrom(member) + .leftJoin(member.missions, mission) + .fetchJoin() + .leftJoin(mission.missionRecords, missionRecord) + .on(missionRecord.createdAt.between(start, end)) + .where( + missionRecord + .isNull() + .or(missionRecord.uploadStatus.ne(ImageUploadStatus.COMPLETE)), + member.fcmInfo.fcmToken.isNotNull(), + mission.startedAt.loe(today), + mission.finishedAt.goe(today)) + .fetch(); + } +} diff --git a/src/main/java/com/depromeet/scheduler/member/MemberBatchScheduler.java b/src/main/java/com/depromeet/scheduler/member/MemberBatchScheduler.java new file mode 100644 index 000000000..98281b126 --- /dev/null +++ b/src/main/java/com/depromeet/scheduler/member/MemberBatchScheduler.java @@ -0,0 +1,20 @@ +package com.depromeet.scheduler.member; + +import com.depromeet.domain.member.application.MemberService; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +@Slf4j +public class MemberBatchScheduler { + private final MemberService memberService; + + @Scheduled(cron = "0 0 22 * * *") + public void pushNotificationByMissionRequest() { + log.info("PushNotification MissionRequest execute"); + memberService.pushNotificationMissionRequest(); + } +} From 6092490c490cb3bd6d104f7f15d7450023a4c632 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=AA=A8?= Date: Fri, 9 Feb 2024 23:38:23 +0900 Subject: [PATCH 3/4] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EB=8B=A8?= =?UTF-8?q?=EA=B1=B4=EC=A1=B0=ED=9A=8C=20=EC=9D=91=EB=8B=B5=20=EA=B0=92?= =?UTF-8?q?=EC=97=90=20=EB=AF=B8=EC=85=98=20=EC=8B=9C=EC=9E=91=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=EA=B3=BC=20=EB=81=9D=EB=82=98=EB=8A=94=20=EC=8B=9C?= =?UTF-8?q?=EA=B0=84=20=EC=B6=94=EA=B0=80=20(#304)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 미션 단건조회 응답 값에 미션 시작시간과 끝나는 시간 추가 * test: 미션 단건조회 테스트코드 수정 --- .../dto/response/MissionFindResponse.java | 26 +++++++++++++++++-- .../controller/MissionControllerTest.java | 4 ++- 2 files changed, 27 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/depromeet/domain/mission/dto/response/MissionFindResponse.java b/src/main/java/com/depromeet/domain/mission/dto/response/MissionFindResponse.java index abd371520..81531d962 100644 --- a/src/main/java/com/depromeet/domain/mission/dto/response/MissionFindResponse.java +++ b/src/main/java/com/depromeet/domain/mission/dto/response/MissionFindResponse.java @@ -5,7 +5,9 @@ import com.depromeet.domain.mission.domain.Mission; import com.depromeet.domain.mission.domain.MissionCategory; import com.depromeet.domain.mission.domain.MissionVisibility; +import com.fasterxml.jackson.annotation.JsonFormat; import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; public record MissionFindResponse( @Schema(description = "미션 ID", defaultValue = "1") Long missionId, @@ -16,7 +18,25 @@ public record MissionFindResponse( @Schema(description = "미션 진행 여부", defaultValue = "IN_PROGRESS") DurationStatus durationStatus, @Schema(description = "미션 아카이빙 상태", defaultValue = "NONE") ArchiveStatus status, - @Schema(description = "미션 정렬 값", defaultValue = "1") Integer sort) { + @Schema(description = "미션 정렬 값", defaultValue = "1") Integer sort, + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + @Schema( + description = "미션 시작 시간", + defaultValue = "2023-01-03 00:00:00", + type = "string") + LocalDateTime startedAt, + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + @Schema( + description = "미션 종료 시간", + defaultValue = "2024-01-03 00:34:00", + type = "string") + LocalDateTime finishedAt) { public static MissionFindResponse from(Mission mission) { return new MissionFindResponse( @@ -27,6 +47,8 @@ public static MissionFindResponse from(Mission mission) { mission.getVisibility(), mission.getDurationStatus(), mission.getArchiveStatus(), - mission.getSort()); + mission.getSort(), + mission.getStartedAt(), + mission.getFinishedAt()); } } diff --git a/src/test/java/com/depromeet/domain/mission/controller/MissionControllerTest.java b/src/test/java/com/depromeet/domain/mission/controller/MissionControllerTest.java index 0a10acb03..556e2c6f1 100644 --- a/src/test/java/com/depromeet/domain/mission/controller/MissionControllerTest.java +++ b/src/test/java/com/depromeet/domain/mission/controller/MissionControllerTest.java @@ -123,7 +123,9 @@ class MissionControllerTest { MissionVisibility.ALL, DurationStatus.IN_PROGRESS, ArchiveStatus.NONE, - 1)); + 1, + LocalDateTime.of(2024, 01, 01, 1, 5, 0), + LocalDateTime.of(2024, 01, 15, 1, 5, 0))); // when, then ResultActions perform = From d0576e055df53f6a4d6e1a769ec8908b127f4aea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=AA=A8?= Date: Sat, 10 Feb 2024 01:30:16 +0900 Subject: [PATCH 4/4] =?UTF-8?q?hotfix:=20=ED=94=BC=EB=93=9C=20Response?= =?UTF-8?q?=EC=97=90=20=EB=AF=B8=EC=85=98=20=EC=8B=9C=EC=9E=91=ED=95=9C?= =?UTF-8?q?=EC=A7=80=20N=EC=9D=BC=EC=B0=A8(sinceDay)=20=EB=8D=B0=EC=9D=B4?= =?UTF-8?q?=ED=84=B0=20=EA=B0=92=EC=9D=B4=20=EC=98=AC=EB=B0=94=EB=A5=B4?= =?UTF-8?q?=EA=B2=8C=20=EB=82=98=EC=98=A4=EC=A7=80=20=EC=95=8A=EB=8A=94=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20(#308)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/feed/dto/response/FeedOneByProfileResponse.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/depromeet/domain/feed/dto/response/FeedOneByProfileResponse.java b/src/main/java/com/depromeet/domain/feed/dto/response/FeedOneByProfileResponse.java index 16577d97f..5eb26b546 100644 --- a/src/main/java/com/depromeet/domain/feed/dto/response/FeedOneByProfileResponse.java +++ b/src/main/java/com/depromeet/domain/feed/dto/response/FeedOneByProfileResponse.java @@ -42,7 +42,8 @@ public static FeedOneByProfileResponse of(MissionRecord record) { record.getMission().getName(), record.getImageUrl(), record.getDuration().toMinutes(), - Duration.between(record.getStartedAt(), LocalDateTime.now()).toDays() + 1, + Duration.between(record.getMission().getStartedAt(), LocalDateTime.now()).toDays() + + 1, record.getStartedAt(), record.getFinishedAt()); }