From df1fff8ae9ed28ec37a763962c6521515aa66432 Mon Sep 17 00:00:00 2001 From: kdomo Date: Sun, 31 Mar 2024 22:58:23 +0900 Subject: [PATCH 1/9] =?UTF-8?q?feat:=20MissionPeriod=20=EB=8F=84=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/mission/domain/MissionPeriod.java | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 src/main/java/com/depromeet/domain/mission/domain/MissionPeriod.java diff --git a/src/main/java/com/depromeet/domain/mission/domain/MissionPeriod.java b/src/main/java/com/depromeet/domain/mission/domain/MissionPeriod.java new file mode 100644 index 000000000..ae05e770e --- /dev/null +++ b/src/main/java/com/depromeet/domain/mission/domain/MissionPeriod.java @@ -0,0 +1,19 @@ +package com.depromeet.domain.mission.domain; + +import java.time.Period; +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum MissionPeriod { + TWO_WEEKS("2주", Period.ofWeeks(2)), + ONE_MONTH("1개월", Period.ofMonths(1)), + TWO_MONTHS("2개월", Period.ofMonths(2)), + THREE_MONTHS("3개월", Period.ofMonths(3)), + FOUR_MONTHS("4개월", Period.ofMonths(4)), + ; + + private final String value; + private final Period period; +} From bc42602192457fca98b4350a6d722349f8f96db8 Mon Sep 17 00:00:00 2001 From: kdomo Date: Sun, 31 Mar 2024 22:58:34 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20=EB=AF=B8=EC=85=98=20=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=EC=8B=9C=20=EB=AF=B8=EC=85=98=20=EA=B8=B0=EA=B0=84?= =?UTF-8?q?=20=EC=84=A4=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../depromeet/domain/mission/application/MissionService.java | 2 +- .../domain/mission/dto/request/MissionCreateRequest.java | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/depromeet/domain/mission/application/MissionService.java b/src/main/java/com/depromeet/domain/mission/application/MissionService.java index 3c5490670..8b71d0064 100644 --- a/src/main/java/com/depromeet/domain/mission/application/MissionService.java +++ b/src/main/java/com/depromeet/domain/mission/application/MissionService.java @@ -284,7 +284,7 @@ private Mission createMissionEntity(MissionCreateRequest missionCreateRequest) { missionCreateRequest.category(), missionCreateRequest.visibility(), startedAt, - startedAt.plusWeeks(2), + startedAt.plus(missionCreateRequest.period().getPeriod()), missionCreateRequest.remindAt(), member); } diff --git a/src/main/java/com/depromeet/domain/mission/dto/request/MissionCreateRequest.java b/src/main/java/com/depromeet/domain/mission/dto/request/MissionCreateRequest.java index 60f43b192..1fc73f380 100644 --- a/src/main/java/com/depromeet/domain/mission/dto/request/MissionCreateRequest.java +++ b/src/main/java/com/depromeet/domain/mission/dto/request/MissionCreateRequest.java @@ -1,6 +1,7 @@ package com.depromeet.domain.mission.dto.request; import com.depromeet.domain.mission.domain.MissionCategory; +import com.depromeet.domain.mission.domain.MissionPeriod; import com.depromeet.domain.mission.domain.MissionVisibility; import io.swagger.v3.oas.annotations.media.Schema; import jakarta.validation.constraints.NotBlank; @@ -19,5 +20,6 @@ public record MissionCreateRequest( @NotNull @Schema(description = "미션 카테고리", defaultValue = "STUDY") MissionCategory category, @NotNull @Schema(description = "미션 공개여부", defaultValue = "ALL") MissionVisibility visibility, + @NotNull @Schema(description = "미션 기한", defaultValue = "TWO_WEEKS") MissionPeriod period, @Schema(description = "미션 리마인드 알림 시간", defaultValue = "00:50:00", type = "string") LocalTime remindAt) {} From 0c1035f1e6a73ac48698ed8cb47a8e55cdcd4120 Mon Sep 17 00:00:00 2001 From: kdomo Date: Sun, 31 Mar 2024 22:58:40 +0900 Subject: [PATCH 3/9] =?UTF-8?q?test:=20=EB=AF=B8=EC=85=98=20=EA=B8=B0?= =?UTF-8?q?=EA=B0=84=20=EC=84=A4=EC=A0=95=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=EC=97=90=20=EB=94=B0=EB=A5=B8=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/image/application/ImageServiceTest.java | 4 ++++ .../domain/mission/controller/MissionControllerTest.java | 8 +++----- .../domain/mission/repository/MissionRepositoryTest.java | 5 +++++ .../domain/mission/service/MissionServiceTest.java | 9 +++++++++ 4 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/test/java/com/depromeet/domain/image/application/ImageServiceTest.java b/src/test/java/com/depromeet/domain/image/application/ImageServiceTest.java index 3fba696ca..ef7840232 100644 --- a/src/test/java/com/depromeet/domain/image/application/ImageServiceTest.java +++ b/src/test/java/com/depromeet/domain/image/application/ImageServiceTest.java @@ -12,6 +12,7 @@ import com.depromeet.domain.member.domain.Profile; import com.depromeet.domain.mission.application.MissionService; import com.depromeet.domain.mission.domain.MissionCategory; +import com.depromeet.domain.mission.domain.MissionPeriod; import com.depromeet.domain.mission.domain.MissionVisibility; import com.depromeet.domain.mission.dto.request.MissionCreateRequest; import com.depromeet.domain.mission.dto.response.MissionCreateResponse; @@ -97,6 +98,7 @@ class 미션_기록_이미지_PresignedUrl을_생성할_때 { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); MissionCreateResponse missionCreateResponse = missionService.createMission(missionCreateRequest); @@ -136,6 +138,7 @@ class 미션_기록_이미지_PresignedUrl을_생성할_때 { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); MissionCreateResponse missionCreateResponse = missionService.createMission(missionCreateRequest); @@ -214,6 +217,7 @@ class 미션_기록_이미지_업로드_완료_처리할_때 { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); MissionCreateResponse missionCreateResponse = missionService.createMission(missionCreateRequest); 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 9bed30d39..4dd2337bf 100644 --- a/src/test/java/com/depromeet/domain/mission/controller/MissionControllerTest.java +++ b/src/test/java/com/depromeet/domain/mission/controller/MissionControllerTest.java @@ -13,11 +13,7 @@ import com.depromeet.domain.member.domain.Profile; import com.depromeet.domain.mission.api.MissionController; import com.depromeet.domain.mission.application.MissionService; -import com.depromeet.domain.mission.domain.ArchiveStatus; -import com.depromeet.domain.mission.domain.DurationStatus; -import com.depromeet.domain.mission.domain.Mission; -import com.depromeet.domain.mission.domain.MissionCategory; -import com.depromeet.domain.mission.domain.MissionVisibility; +import com.depromeet.domain.mission.domain.*; import com.depromeet.domain.mission.dto.request.MissionCreateRequest; import com.depromeet.domain.mission.dto.request.MissionUpdateRequest; import com.depromeet.domain.mission.dto.response.*; @@ -59,6 +55,7 @@ class MissionControllerTest { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); given(missionService.createMission(any())) @@ -98,6 +95,7 @@ class MissionControllerTest { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); // when, then diff --git a/src/test/java/com/depromeet/domain/mission/repository/MissionRepositoryTest.java b/src/test/java/com/depromeet/domain/mission/repository/MissionRepositoryTest.java index 1db365739..8ffabdaae 100644 --- a/src/test/java/com/depromeet/domain/mission/repository/MissionRepositoryTest.java +++ b/src/test/java/com/depromeet/domain/mission/repository/MissionRepositoryTest.java @@ -10,6 +10,7 @@ import com.depromeet.domain.mission.dao.MissionRepository; import com.depromeet.domain.mission.domain.Mission; import com.depromeet.domain.mission.domain.MissionCategory; +import com.depromeet.domain.mission.domain.MissionPeriod; import com.depromeet.domain.mission.domain.MissionVisibility; import com.depromeet.domain.mission.dto.request.MissionCreateRequest; import java.time.LocalDateTime; @@ -54,6 +55,7 @@ void setUp() { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); // when @@ -89,6 +91,7 @@ void setUp() { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); // when @@ -119,6 +122,7 @@ void setUp() { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); LocalDateTime startedAt = LocalDateTime.now(); Mission saveMission = @@ -160,6 +164,7 @@ void setUp() { "testMissionContent_" + i, MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0))) .forEach( request -> diff --git a/src/test/java/com/depromeet/domain/mission/service/MissionServiceTest.java b/src/test/java/com/depromeet/domain/mission/service/MissionServiceTest.java index 54423fa8d..dac47f9d5 100644 --- a/src/test/java/com/depromeet/domain/mission/service/MissionServiceTest.java +++ b/src/test/java/com/depromeet/domain/mission/service/MissionServiceTest.java @@ -11,6 +11,7 @@ import com.depromeet.domain.mission.dao.MissionRepository; import com.depromeet.domain.mission.domain.Mission; import com.depromeet.domain.mission.domain.MissionCategory; +import com.depromeet.domain.mission.domain.MissionPeriod; import com.depromeet.domain.mission.domain.MissionVisibility; import com.depromeet.domain.mission.dto.request.MissionCreateRequest; import com.depromeet.domain.mission.dto.request.MissionUpdateRequest; @@ -69,6 +70,7 @@ void setUp() { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); // when @@ -91,6 +93,7 @@ void setUp() { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); MissionCreateResponse saveMission = missionService.createMission(missionCreateRequest); @@ -118,6 +121,7 @@ void setUp() { "testMissionContent_" + i, MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0))) .forEach( request -> @@ -156,6 +160,7 @@ void setUp() { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); MissionCreateResponse saveMission = missionService.createMission(missionCreateRequest); MissionUpdateRequest missionUpdateRequest = @@ -179,6 +184,7 @@ void setUp() { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); MissionCreateResponse saveMission = missionService.createMission(missionCreateRequest); MissionUpdateRequest missionUpdateRequest = @@ -202,6 +208,7 @@ void setUp() { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); MissionCreateResponse saveMission = missionService.createMission(missionCreateRequest); MissionUpdateRequest missionUpdateRequest = @@ -229,6 +236,7 @@ void setUp() { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); MissionCreateResponse saveMission = missionService.createMission(missionCreateRequest); @@ -249,6 +257,7 @@ void setUp() { "testMissionContent", MissionCategory.STUDY, MissionVisibility.ALL, + MissionPeriod.TWO_WEEKS, LocalTime.of(21, 0)); missionService.createMission(missionCreateRequest); From 50e5bcbffbfbb7b76d84ae6ff7ce542a2f41fc28 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Wed, 3 Apr 2024 10:32:36 +0900 Subject: [PATCH 4/9] =?UTF-8?q?=F0=9F=90=9B=2012=EC=8B=9C=20=EC=9D=B4?= =?UTF-8?q?=ED=9B=84=20=EB=AF=B8=EC=85=98=20=EC=9D=B8=EC=A6=9D=20=EC=95=88?= =?UTF-8?q?=EB=90=98=EB=8A=94=20=EC=97=90=EB=9F=AC=20=EC=B2=98=EB=A6=AC=20?= =?UTF-8?q?(#380)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 미션 인증 초과 validate * fix: spotlessApply * fix: magic number 상수화 * fix: 에러 코드 변경 * fix: spotlessApply --- .../application/MissionRecordService.java | 10 ++++++++++ .../depromeet/global/error/exception/ErrorCode.java | 1 + .../application/MissionRecordServiceTest.java | 11 +++++++++++ 3 files changed, 22 insertions(+) diff --git a/src/main/java/com/depromeet/domain/missionRecord/application/MissionRecordService.java b/src/main/java/com/depromeet/domain/missionRecord/application/MissionRecordService.java index 450241496..aef580838 100644 --- a/src/main/java/com/depromeet/domain/missionRecord/application/MissionRecordService.java +++ b/src/main/java/com/depromeet/domain/missionRecord/application/MissionRecordService.java @@ -31,6 +31,7 @@ public class MissionRecordService { private static final int EXPIRATION_TIME = 10; private static final int DAYS_ADJUSTMENT = 1; + private static final long MAX_DURATION_HOUR = 24; private final MemberUtil memberUtil; private final MissionRepository missionRepository; @@ -38,6 +39,9 @@ public class MissionRecordService { private final MissionRecordTtlRepository missionRecordTtlRepository; public MissionRecordCreateResponse createMissionRecord(MissionRecordCreateRequest request) { + long diffHour = Duration.between(request.startedAt(), request.finishedAt()).toHours(); + validateMissionRecordDurationOverTime(diffHour); + final Mission mission = findMissionById(request.missionId()); final Member member = memberUtil.getCurrentMember(); @@ -65,6 +69,12 @@ public MissionRecordCreateResponse createMissionRecord(MissionRecordCreateReques return MissionRecordCreateResponse.from(createdMissionRecord.getId()); } + private void validateMissionRecordDurationOverTime(long diffHour) { + if (diffHour >= MAX_DURATION_HOUR) { + throw new CustomException(ErrorCode.MISSION_RECORD_DURATION_OVERTIME); + } + } + private void validateMissionRecordExistsToday(Long missionId) { if (missionRecordRepository.isCompletedMissionExistsToday(missionId)) { throw new CustomException(ErrorCode.MISSION_RECORD_ALREADY_EXISTS_TODAY); diff --git a/src/main/java/com/depromeet/global/error/exception/ErrorCode.java b/src/main/java/com/depromeet/global/error/exception/ErrorCode.java index 7b9466cae..96100e812 100644 --- a/src/main/java/com/depromeet/global/error/exception/ErrorCode.java +++ b/src/main/java/com/depromeet/global/error/exception/ErrorCode.java @@ -44,6 +44,7 @@ public enum ErrorCode { MISSION_RECORD_UPLOAD_STATUS_IS_NOT_PENDING( HttpStatus.BAD_REQUEST, "미션 기록의 이미지 업로드 상태가 PENDING이 아닙니다."), MISSION_RECORD_ALREADY_EXISTS_TODAY(HttpStatus.BAD_REQUEST, "오늘 이미 작성 된 미션 기록이 존재합니다."), + MISSION_RECORD_DURATION_OVERTIME(HttpStatus.CONFLICT, "가능한 미션 인증 시간이 초과되었습니다."), // Follow FOLLOW_TARGET_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "타겟 유저을 찾을 수 없습니다."), diff --git a/src/test/java/com/depromeet/domain/missionRecord/application/MissionRecordServiceTest.java b/src/test/java/com/depromeet/domain/missionRecord/application/MissionRecordServiceTest.java index 219b2e2ba..1ecacf0c0 100644 --- a/src/test/java/com/depromeet/domain/missionRecord/application/MissionRecordServiceTest.java +++ b/src/test/java/com/depromeet/domain/missionRecord/application/MissionRecordServiceTest.java @@ -71,6 +71,17 @@ void setUp() { missionRepository.save(mission); } + @Test + void 하루가_지나_미션_인증한_경우_에러를_발생시킨다() { + // exception + assertThrows( + CustomException.class, + () -> + missionRecordService.createMissionRecord( + new MissionRecordCreateRequest( + mission.getId(), now, now.plusDays(1), 20, 0))); + } + @Test void 진행중인_미션기록을_삭제한다() { // given From d9e60b71c8dcaeba2cdca23f38be3d90ad156151 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Fri, 5 Apr 2024 10:12:15 +0900 Subject: [PATCH 5/9] =?UTF-8?q?fix:=20=EB=A6=AC=EB=A7=88=EC=9D=B8=EB=93=9C?= =?UTF-8?q?=20=ED=91=B8=EC=8B=9C=20=EC=95=8C=EB=A6=BC=ED=95=A0=20=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EB=8C=80=EC=83=81=20=EC=A1=B0=EA=B1=B4=20=EC=B2=98?= =?UTF-8?q?=EB=A6=AC=20(#382)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 리마인드 푸시 알림할 미션 대상 조건 처리 * fix: cron식 수정 --- .../member/dao/MemberRepositoryImpl.java | 2 ++ .../mission/application/MissionService.java | 5 ++--- .../domain/mission/dao/MissionRepository.java | 8 -------- .../mission/dao/MissionRepositoryCustom.java | 2 ++ .../mission/dao/MissionRepositoryImpl.java | 19 +++++++++++++++++++ .../constants/PushNotificationConstants.java | 2 +- .../mission/MissionBatchScheduler.java | 8 ++++---- 7 files changed, 30 insertions(+), 16 deletions(-) diff --git a/src/main/java/com/depromeet/domain/member/dao/MemberRepositoryImpl.java b/src/main/java/com/depromeet/domain/member/dao/MemberRepositoryImpl.java index 4db6e264a..f034b8c94 100644 --- a/src/main/java/com/depromeet/domain/member/dao/MemberRepositoryImpl.java +++ b/src/main/java/com/depromeet/domain/member/dao/MemberRepositoryImpl.java @@ -5,6 +5,7 @@ import static com.depromeet.domain.missionRecord.domain.QMissionRecord.*; import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.mission.domain.DurationStatus; import com.depromeet.domain.missionRecord.domain.ImageUploadStatus; import com.querydsl.jpa.impl.JPAQueryFactory; import java.time.LocalDateTime; @@ -32,6 +33,7 @@ public List findMissionNonCompletedMembers(LocalDateTime today) { missionRecord .isNull() .or(missionRecord.uploadStatus.ne(ImageUploadStatus.COMPLETE)), + mission.durationStatus.eq(DurationStatus.IN_PROGRESS), member.fcmInfo.fcmToken.isNotNull(), mission.startedAt.loe(today), mission.finishedAt.goe(today)) diff --git a/src/main/java/com/depromeet/domain/mission/application/MissionService.java b/src/main/java/com/depromeet/domain/mission/application/MissionService.java index 8b71d0064..22f64f54b 100644 --- a/src/main/java/com/depromeet/domain/mission/application/MissionService.java +++ b/src/main/java/com/depromeet/domain/mission/application/MissionService.java @@ -3,7 +3,6 @@ import com.depromeet.domain.follow.dao.MemberRelationRepository; import com.depromeet.domain.member.domain.Member; import com.depromeet.domain.mission.dao.MissionRepository; -import com.depromeet.domain.mission.domain.DurationStatus; import com.depromeet.domain.mission.domain.Mission; import com.depromeet.domain.mission.dto.request.MissionCreateRequest; import com.depromeet.domain.mission.dto.request.MissionUpdateRequest; @@ -244,8 +243,8 @@ public List findAllFinishedMission() { @Transactional(readOnly = true) public List findAllInProgressMission() { - List missions = - missionRepository.findAllByDurationStatus(DurationStatus.IN_PROGRESS); + LocalDateTime today = LocalDateTime.now(); + List missions = missionRepository.findMissionsNonCompleteAndInProgress(today); return missions.stream().map(MissionRemindPushResponse::from).toList(); } diff --git a/src/main/java/com/depromeet/domain/mission/dao/MissionRepository.java b/src/main/java/com/depromeet/domain/mission/dao/MissionRepository.java index 0f10167d7..858116aee 100644 --- a/src/main/java/com/depromeet/domain/mission/dao/MissionRepository.java +++ b/src/main/java/com/depromeet/domain/mission/dao/MissionRepository.java @@ -3,17 +3,9 @@ import static org.springframework.data.jpa.repository.EntityGraph.EntityGraphType.*; import com.depromeet.domain.member.domain.Member; -import com.depromeet.domain.mission.domain.DurationStatus; import com.depromeet.domain.mission.domain.Mission; -import java.util.List; -import org.springframework.data.jpa.repository.EntityGraph; import org.springframework.data.jpa.repository.JpaRepository; public interface MissionRepository extends JpaRepository, MissionRepositoryCustom { Mission findTopByMemberOrderBySortDesc(Member member); - - @EntityGraph( - attributePaths = {"member"}, - type = FETCH) - List findAllByDurationStatus(DurationStatus status); } diff --git a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryCustom.java b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryCustom.java index 81ee397d2..c11d586d2 100644 --- a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryCustom.java +++ b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryCustom.java @@ -18,4 +18,6 @@ public interface MissionRepositoryCustom { List findAllFinishedMission(Long memberId); List findMissionsWithRecordsByDate(LocalDate date, Long memberId); + + List findMissionsNonCompleteAndInProgress(LocalDateTime today); } diff --git a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java index 2d38d4d51..7df8768a3 100644 --- a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java +++ b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java @@ -7,6 +7,7 @@ import com.depromeet.domain.mission.domain.DurationStatus; import com.depromeet.domain.mission.domain.Mission; import com.depromeet.domain.mission.domain.MissionVisibility; +import com.depromeet.domain.missionRecord.domain.ImageUploadStatus; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.core.types.dsl.Expressions; import com.querydsl.jpa.impl.JPAQuery; @@ -104,6 +105,24 @@ public List findMissionsWithRecordsByDate(LocalDate date, Long memberId return query.fetch(); } + @Override + public List findMissionsNonCompleteAndInProgress(LocalDateTime today) { + return jpaQueryFactory + .selectFrom(mission) + .leftJoin(mission.member, member) + .fetchJoin() + .leftJoin(mission.missionRecords, missionRecord) + .on(missionRecord.createdAt.loe(today)) + .where( + missionRecord + .isNull() + .or(missionRecord.uploadStatus.ne(ImageUploadStatus.COMPLETE)), + mission.durationStatus.eq(DurationStatus.IN_PROGRESS), + mission.startedAt.loe(today), + mission.finishedAt.goe(today)) + .fetch(); + } + // 미션의 사용자 id 조건 검증 메서드 private BooleanExpression memberIdEq(Long memberId) { return memberId == null ? null : mission.member.id.eq(memberId); diff --git a/src/main/java/com/depromeet/global/common/constants/PushNotificationConstants.java b/src/main/java/com/depromeet/global/common/constants/PushNotificationConstants.java index a21e681f0..e280bd717 100644 --- a/src/main/java/com/depromeet/global/common/constants/PushNotificationConstants.java +++ b/src/main/java/com/depromeet/global/common/constants/PushNotificationConstants.java @@ -16,5 +16,5 @@ public class PushNotificationConstants { public static final String PUSH_MISSION_REMIND_TITLE = "10분이 지났어요!"; public static final String PUSH_MISSION_REMIND_CONTENT = "지금부터 미션 인증을 할 수 있어요 🕑"; public static final String PUSH_MISSION_START_REMIND_TITLE = "미션을 시작할 시간이에요!"; - public static final String PUSH_MISSION_START_REMIND_CONTENT = "10분을 투자하여 %s 미션을 완료해 보세요 🥳"; + public static final String PUSH_MISSION_START_REMIND_CONTENT = "10분만 투자해서 %s 미션을 완료해봐요 🥳"; } diff --git a/src/main/java/com/depromeet/scheduler/mission/MissionBatchScheduler.java b/src/main/java/com/depromeet/scheduler/mission/MissionBatchScheduler.java index bc64b5a31..a8326b055 100644 --- a/src/main/java/com/depromeet/scheduler/mission/MissionBatchScheduler.java +++ b/src/main/java/com/depromeet/scheduler/mission/MissionBatchScheduler.java @@ -37,13 +37,13 @@ public void missionRemindPushNotification() { inProgressMissions.forEach(this::sendMissionRemindPushNotification); } - private void sendMissionRemindPushNotification(MissionRemindPushResponse mission) { + private void sendMissionRemindPushNotification(MissionRemindPushResponse remindPushResponse) { LocalTime currentTime = LocalTime.now().truncatedTo(ChronoUnit.MINUTES); - if (currentTime.equals(mission.remindAt())) { + if (currentTime.equals(remindPushResponse.remindAt())) { fcmService.sendMessageSync( - mission.fcmToken(), + remindPushResponse.fcmToken(), PUSH_MISSION_START_REMIND_TITLE, - String.format(PUSH_MISSION_START_REMIND_CONTENT, mission.name())); + String.format(PUSH_MISSION_START_REMIND_CONTENT, remindPushResponse.name())); } } } From 8b02ff9d4ba1deca9897850d98164352b83276d5 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Thu, 11 Apr 2024 09:59:55 +0900 Subject: [PATCH 6/9] =?UTF-8?q?refactor:=20=ED=94=BC=EB=93=9C=20=EB=A6=AC?= =?UTF-8?q?=ED=8C=A9=ED=86=A0=EB=A7=81=20=EB=B0=8F=20=EC=BD=94=EB=A9=98?= =?UTF-8?q?=ED=8A=B8=20=EC=A1=B0=ED=9A=8C=20=EA=B8=B0=EB=8A=A5=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20(#383)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: toList를 사용하도록 개선 * style: spotless 적용 * feat: 전체 피드 deprecated 처리 * refactor: 컨트롤러에서 로직을 수행하지 않도록 개선 * chore: import 추가 * docs: 미션 수행시간 설명을 더 명료하게 수정 * chore: 임포트 수정 * docs: 잘못된 DTO 필드 설명 수정 * feat: 피드 응답에 리액션, 댓글 목록 추가 * feat: 미사용 정적 팩토리 메서드 제거 * refactor: sinceDay 계산로직 추출 * docs: 투두 주석 추가 * style: spotless 적용 * feat: 피드 조회 V2 API 추가 * feat: 피드 조회 v2 서비스 로직 구현 * feat: 미션기록을 인자로 받는 정적 팩토리 메서드 추가 * feat: 미션기록 조회시 페치 조인하는 로직 구현 * feat: 무한 스크롤 위한 hasNext 메서드 추가 * feat: 리액션 그룹 DTO 내부에 컬렉션 변환 로직 추가 * feat: 팔로워 피드 임시 로직 * test: 피드 조회 v2 테스트 * chore: 테스트 설정의 SQL 로그 및 배치사이즈 옵션 설정 * fix: 하나의 일대다 페치조인만 사용하도록 수정 * test: 테스트 오류 수정 * feat: 피드 공개여부 상태 추가 * feat: 팔로잉 피드 조회 * refactor: 비공개 메서드로 변경 * chore: 임포트 추가 * test: 임포트 추가 * test: 로그인 / 로그아웃 유틸 메서드 추가 * test: 픽스처 추가 및 저장 로직 개선 * test: 전체 피드 테스트 추가 * chore: 미션 공개상태 정적 임포트 * fix: 팔로잉 피드 조회 시 where 조건 누락 수정 * test: 팔로잉 픽스처 추가 * test: 팔로잉 피드 테스트 추가 * test: 테스트 로그 옵션 비활성화 * refactor: 메서드 이름 수정 * refactor: findAllBy로 이름 수정 --- .../domain/feed/api/FeedController.java | 19 +- .../domain/feed/application/FeedService.java | 43 ++- .../domain/feed/domain/FeedVisibility.java | 14 + .../feed/dto/response/FeedOneResponse.java | 83 +++-- .../follow/application/FollowService.java | 12 + .../follow/dao/MemberRelationRepository.java | 2 + .../dao/MissionRecordRepositoryCustom.java | 5 + .../dao/MissionRecordRepositoryImpl.java | 61 +++- .../reaction/application/ReactionService.java | 1 + .../ReactionGroupByEmojiResponse.java | 14 + .../feed/application/FeedServiceTest.java | 311 ++++++++++++++++++ src/test/resources/application-test.yml | 5 + 12 files changed, 522 insertions(+), 48 deletions(-) create mode 100644 src/main/java/com/depromeet/domain/feed/domain/FeedVisibility.java create mode 100644 src/test/java/com/depromeet/domain/feed/application/FeedServiceTest.java diff --git a/src/main/java/com/depromeet/domain/feed/api/FeedController.java b/src/main/java/com/depromeet/domain/feed/api/FeedController.java index bc0c1c80d..62a24f81e 100644 --- a/src/main/java/com/depromeet/domain/feed/api/FeedController.java +++ b/src/main/java/com/depromeet/domain/feed/api/FeedController.java @@ -1,6 +1,7 @@ package com.depromeet.domain.feed.api; import com.depromeet.domain.feed.application.FeedService; +import com.depromeet.domain.feed.domain.FeedVisibility; import com.depromeet.domain.feed.dto.response.FeedOneByProfileResponse; import com.depromeet.domain.feed.dto.response.FeedOneResponse; import com.depromeet.domain.feed.dto.response.FeedSliceResponse; @@ -9,6 +10,8 @@ import io.swagger.v3.oas.annotations.tags.Tag; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; @@ -23,6 +26,7 @@ public class FeedController { private final FeedService feedService; + @Deprecated @Operation(summary = "피드 탭", description = "피드 탭을 조회합니다.") @GetMapping public List feedFindAll( @@ -36,13 +40,14 @@ public FeedSliceResponse feedFindByPage( @RequestParam int size, @RequestParam(required = false) Long lastId, @RequestParam(value = "visibility", required = false) MissionVisibility visibility) { - if (visibility == MissionVisibility.ALL) { - // 전체 피드 탭 - return feedService.findAllFeed(size, lastId); - } else { - // 팔로워 피드 탭 - return feedService.findFollowerFeed(size, lastId); - } + return feedService.findFeed(size, lastId, visibility); + } + + @Operation(summary = "피드 탭 V2 (페이지네이션)", description = "피드 탭을 조회합니다.") + @GetMapping("/me/v2") + public Slice feedFindByPageV2( + @RequestParam(required = false) FeedVisibility visibility, Pageable pageable) { + return feedService.findFeedV2(visibility, pageable); } @Operation(summary = "프로필 피드", description = "피드 탭을 조회합니다.") diff --git a/src/main/java/com/depromeet/domain/feed/application/FeedService.java b/src/main/java/com/depromeet/domain/feed/application/FeedService.java index bc92ad343..bebf1ab2a 100644 --- a/src/main/java/com/depromeet/domain/feed/application/FeedService.java +++ b/src/main/java/com/depromeet/domain/feed/application/FeedService.java @@ -1,21 +1,25 @@ package com.depromeet.domain.feed.application; +import com.depromeet.domain.feed.domain.FeedVisibility; import com.depromeet.domain.feed.dto.response.FeedOneByProfileResponse; import com.depromeet.domain.feed.dto.response.FeedOneResponse; import com.depromeet.domain.feed.dto.response.FeedSliceResponse; +import com.depromeet.domain.follow.application.FollowService; import com.depromeet.domain.follow.dao.MemberRelationRepository; import com.depromeet.domain.follow.domain.MemberRelation; import com.depromeet.domain.member.dao.MemberRepository; import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.mission.dao.MissionRepository; import com.depromeet.domain.mission.domain.MissionVisibility; import com.depromeet.domain.missionRecord.dao.MissionRecordRepository; import com.depromeet.domain.missionRecord.domain.MissionRecord; +import com.depromeet.domain.reaction.application.ReactionService; import com.depromeet.global.util.MemberUtil; import com.depromeet.global.util.SecurityUtil; import java.util.ArrayList; import java.util.List; -import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -25,12 +29,16 @@ @RequiredArgsConstructor @Transactional public class FeedService { + private final ReactionService reactionService; private final MemberUtil memberUtil; + private final MissionRepository missionRepository; private final MissionRecordRepository missionRecordRepository; private final MemberRelationRepository memberRelationRepository; private final SecurityUtil securityUtil; private final MemberRepository memberRepository; + private final FollowService followService; + @Deprecated @Transactional(readOnly = true) public List findAllFeedByVisibility(MissionVisibility visibilities) { if (visibilities == MissionVisibility.ALL) { @@ -45,8 +53,36 @@ public List findAllFeedByVisibility(MissionVisibility visibilit return missionRecordRepository.findFeedAll(sourceMembers); } - // 전체 피드 탭 @Transactional(readOnly = true) + public FeedSliceResponse findFeed(int size, Long lastId, MissionVisibility visibility) { + if (visibility == MissionVisibility.ALL) { + return findAllFeed(size, lastId); + } + return findFollowerFeed(size, lastId); + } + + @Transactional(readOnly = true) + public Slice findFeedV2(FeedVisibility visibility, Pageable pageable) { + if (visibility == FeedVisibility.ALL) { + return findAllFeedV2(pageable); + } + return findFollowingFeedV2(pageable); + } + + private Slice findAllFeedV2(Pageable pageable) { + return missionRecordRepository.findAllFetch(pageable).map(FeedOneResponse::from); + } + + private Slice findFollowingFeedV2(Pageable pageable) { + final Member currentMember = memberUtil.getCurrentMember(); + List followingMembers = followService.getFollowingMembers(currentMember); + + return missionRecordRepository + .findAllFetchByFollowings(pageable, followingMembers) + .map(FeedOneResponse::from); + } + + // 전체 피드 탭 public FeedSliceResponse findAllFeed(int size, Long lastId) { final List members = memberRepository.findAll(); Slice feedByVisibilityAndPage = @@ -56,7 +92,6 @@ public FeedSliceResponse findAllFeed(int size, Long lastId) { } // 팔로워 피드 탭 - @Transactional(readOnly = true) public FeedSliceResponse findFollowerFeed(int size, Long lastId) { final Member currentMember = memberUtil.getCurrentMember(); List sourceMembers = getSourceMembers(currentMember.getId()); @@ -89,7 +124,7 @@ public List findAllFeedByTargetId(Long targetId) { private List getSourceMembers(Long currentMemberId) { return memberRelationRepository.findAllBySourceId(currentMemberId).stream() .map(MemberRelation::getTarget) - .collect(Collectors.toList()); + .toList(); } private boolean isMyFeedRequired(Long targetId, Long sourceId) { diff --git a/src/main/java/com/depromeet/domain/feed/domain/FeedVisibility.java b/src/main/java/com/depromeet/domain/feed/domain/FeedVisibility.java new file mode 100644 index 000000000..eab3d512d --- /dev/null +++ b/src/main/java/com/depromeet/domain/feed/domain/FeedVisibility.java @@ -0,0 +1,14 @@ +package com.depromeet.domain.feed.domain; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +@Getter +@AllArgsConstructor +public enum FeedVisibility { + ALL("전체 피드"), + FOLLOWING("팔로잉 피드"), + ; + + private final String value; +} diff --git a/src/main/java/com/depromeet/domain/feed/dto/response/FeedOneResponse.java b/src/main/java/com/depromeet/domain/feed/dto/response/FeedOneResponse.java index b1cca5f88..14824114f 100644 --- a/src/main/java/com/depromeet/domain/feed/dto/response/FeedOneResponse.java +++ b/src/main/java/com/depromeet/domain/feed/dto/response/FeedOneResponse.java @@ -1,11 +1,20 @@ package com.depromeet.domain.feed.dto.response; +import static com.depromeet.domain.reaction.dto.response.ReactionGroupByEmojiResponse.*; +import static java.util.Comparator.*; + +import com.depromeet.domain.comment.dto.response.CommentDto; +import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.mission.domain.Mission; +import com.depromeet.domain.missionRecord.domain.MissionRecord; +import com.depromeet.domain.reaction.dto.response.ReactionGroupByEmojiResponse; import com.fasterxml.jackson.annotation.JsonFormat; import com.querydsl.core.annotations.QueryProjection; import io.swagger.v3.oas.annotations.media.Schema; import java.time.Duration; import java.time.LocalDateTime; import java.time.temporal.ChronoUnit; +import java.util.List; public record FeedOneResponse( @Schema(description = "작성자 ID", defaultValue = "1") Long memberId, @@ -20,14 +29,14 @@ public record FeedOneResponse( description = "미션 기록 인증 사진 Url", defaultValue = "https://image.10mm.today/default.png") String recordImageUrl, - @Schema(description = "미션 수행한 시간", defaultValue = "21") long duration, + @Schema(description = "미션 수행 시간 (분 단위)", defaultValue = "21") long duration, @Schema(description = "미션 시작한 지 N일차", defaultValue = "3") long sinceDay, @JsonFormat( shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") @Schema( - description = "미션 기록 시작 시간", + description = "미션 시작 일시", defaultValue = "2024-01-06 00:00:00", type = "string") LocalDateTime startedAt, @@ -36,7 +45,7 @@ public record FeedOneResponse( pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") @Schema( - description = "미션 기록 종료 시간", + description = "미션 종료 일시", defaultValue = "2024-01-20 00:34:00", type = "string") LocalDateTime finishedAt, @@ -45,10 +54,12 @@ public record FeedOneResponse( pattern = "yyyy-MM-dd HH:mm:ss", timezone = "Asia/Seoul") @Schema( - description = "미션 기록 시작 시간", + description = "미션 기록 시작 일시", defaultValue = "2024-01-06 00:00:00", type = "string") - LocalDateTime recordStartedAt) { + LocalDateTime recordStartedAt, + @Schema(description = "리액션 타입별 그룹") List reactions, + @Schema(description = "댓글 목록") List comments) { @QueryProjection public FeedOneResponse( Long memberId, @@ -73,38 +84,44 @@ public FeedOneResponse( remark, recordImageUrl, duration.toMinutes(), - ChronoUnit.DAYS.between(startedAt, recordStartedAt) + 1, + calculateSinceDay(startedAt, recordStartedAt), startedAt, finishedAt, - recordStartedAt); + recordStartedAt, + null, + null); } - public static FeedOneResponse of( - Long memberId, - String nickname, - String profileImage, - Long missionId, - String name, - Long recordId, - String remark, - String recordImageUrl, - Duration duration, - LocalDateTime startedAt, - LocalDateTime finishedAt, - LocalDateTime recordStartedAt) { + // TODO: 다른 DTO에 존재하는 sinceDay 중복 계산 로직 제거 + private static long calculateSinceDay(LocalDateTime startedAt, LocalDateTime recordStartedAt) { + return ChronoUnit.DAYS.between(startedAt, recordStartedAt) + 1; + } + + public static FeedOneResponse from(MissionRecord missionRecord) { + Mission mission = missionRecord.getMission(); + Member member = mission.getMember(); + + List reactions = + groupByEmojiType(missionRecord.getReactions()); + + List comments = + missionRecord.getComments().stream().map(CommentDto::from).toList(); + return new FeedOneResponse( - memberId, - nickname, - profileImage, - missionId, - name, - recordId, - remark, - recordImageUrl, - duration.toMinutes(), - Duration.between(startedAt, LocalDateTime.now()).toDays() + 1, - startedAt, - finishedAt, - recordStartedAt); + member.getId(), + member.getProfile().getNickname(), + member.getProfile().getProfileImageUrl(), + mission.getId(), + mission.getName(), + missionRecord.getId(), + missionRecord.getRemark(), + missionRecord.getImageUrl(), + missionRecord.getDuration().toMinutes(), + calculateSinceDay(mission.getStartedAt(), missionRecord.getStartedAt()), + mission.getStartedAt(), + mission.getFinishedAt(), + missionRecord.getStartedAt(), + reactions, + comments); } } 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 ecb6eef42..f02e83666 100644 --- a/src/main/java/com/depromeet/domain/follow/application/FollowService.java +++ b/src/main/java/com/depromeet/domain/follow/application/FollowService.java @@ -266,6 +266,18 @@ public FollowerDeletedResponse deleteFollower(Long targetId) { : FollowerDeletedResponse.from(FollowStatus.NOT_FOLLOWING); } + /** + * 특정 멤버가 팔로우 중인 멤버 목록을 조회합니다. + * + * @param source 특정 멤버 + * @return 팔로우 중인 멤버 목록 + */ + public List getFollowingMembers(Member source) { + return memberRelationRepository.findAllBySource(source).stream() + .map(MemberRelation::getTarget) + .toList(); + } + private static void getFollowStatusIncludeList( List targetMembers, List currentMemberSources, diff --git a/src/main/java/com/depromeet/domain/follow/dao/MemberRelationRepository.java b/src/main/java/com/depromeet/domain/follow/dao/MemberRelationRepository.java index 562ecd790..6d55ddff5 100644 --- a/src/main/java/com/depromeet/domain/follow/dao/MemberRelationRepository.java +++ b/src/main/java/com/depromeet/domain/follow/dao/MemberRelationRepository.java @@ -21,4 +21,6 @@ public interface MemberRelationRepository List findAllBySourceIdAndTargetIn(Long sourceId, List targetIds); List findAllByTargetId(Long targetId); + + List findAllBySource(Member source); } diff --git a/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryCustom.java b/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryCustom.java index b42cd35ff..4c674b07b 100644 --- a/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryCustom.java +++ b/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryCustom.java @@ -6,6 +6,7 @@ import com.depromeet.domain.missionRecord.domain.MissionRecord; import java.time.YearMonth; import java.util.List; +import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Slice; public interface MissionRecordRepositoryCustom { @@ -29,4 +30,8 @@ List findFeedByVisibility( Slice findFeedByVisibilityAndPage( int size, Long lastId, List members, List visibility); + + Slice findAllFetch(Pageable pageable); + + Slice findAllFetchByFollowings(Pageable pageable, List followingMembers); } diff --git a/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryImpl.java b/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryImpl.java index 719868bc1..f5a81ebb8 100644 --- a/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryImpl.java +++ b/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryImpl.java @@ -1,8 +1,10 @@ package com.depromeet.domain.missionRecord.dao; import static com.depromeet.domain.member.domain.QMember.*; +import static com.depromeet.domain.mission.domain.MissionVisibility.*; import static com.depromeet.domain.mission.domain.QMission.*; import static com.depromeet.domain.missionRecord.domain.QMissionRecord.*; +import static com.depromeet.domain.reaction.domain.QReaction.*; import com.depromeet.domain.feed.dto.response.FeedOneResponse; import com.depromeet.domain.member.domain.Member; @@ -67,8 +69,7 @@ public boolean isCompletedMissionExistsToday(Long missionId) { @Override public List findFeedAll(List members) { - return findFeedByVisibility( - members, List.of(MissionVisibility.FOLLOWER, MissionVisibility.ALL)); + return findFeedByVisibility(members, List.of(FOLLOWER, ALL)); } @Override @@ -107,8 +108,7 @@ public List findFeedByVisibility( @Override public Slice findFeedAllByPage(int size, Long lastId, List members) { - return findFeedByVisibilityAndPage( - size, lastId, members, List.of(MissionVisibility.FOLLOWER, MissionVisibility.ALL)); + return findFeedByVisibilityAndPage(size, lastId, members, List.of(FOLLOWER, ALL)); } @Override @@ -167,6 +167,50 @@ public void deleteByMissionRecordId(Long missionRecordId) { jpaQueryFactory.delete(missionRecord).where(missionRecord.id.eq(missionRecordId)).execute(); } + @Override + public Slice findAllFetch(Pageable pageable) { + + List missionRecords = + jpaQueryFactory + .selectFrom(missionRecord) + .join(missionRecord.mission, mission) + .fetchJoin() + .join(mission.member, member) + .fetchJoin() + .leftJoin(missionRecord.reactions, reaction) + .fetchJoin() + .distinct() + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1L) + .fetch(); + + boolean hasNext = getHasNext(missionRecords, pageable); + + return new SliceImpl<>(missionRecords, pageable, hasNext); + } + + @Override + public Slice findAllFetchByFollowings( + Pageable pageable, List followingMembers) { + + List missionRecords = + jpaQueryFactory + .selectFrom(missionRecord) + .join(missionRecord.mission, mission) + .fetchJoin() + .join(mission.member, member) + .fetchJoin() + .where(missionRecord.mission.member.in(followingMembers)) + .where(mission.visibility.in(List.of(ALL, FOLLOWER))) + .offset(pageable.getOffset()) + .limit(pageable.getPageSize() + 1L) + .fetch(); + + boolean hasNext = getHasNext(missionRecords, pageable); + + return new SliceImpl<>(missionRecords, pageable, hasNext); + } + private BooleanExpression missionIdEq(Long missionId) { return missionRecord.mission.id.eq(missionId); } @@ -208,4 +252,13 @@ private Slice checkLastPage(int size, List res Pageable pageable = PageRequest.ofSize(size); return new SliceImpl<>(result, pageable, hasNext); } + + private boolean getHasNext(List list, Pageable pageable) { + boolean hasNext = false; + if (list.size() > pageable.getPageSize()) { + list.remove(pageable.getPageSize()); + hasNext = true; + } + return hasNext; + } } diff --git a/src/main/java/com/depromeet/domain/reaction/application/ReactionService.java b/src/main/java/com/depromeet/domain/reaction/application/ReactionService.java index 5fed4beb3..fbd4d3edb 100644 --- a/src/main/java/com/depromeet/domain/reaction/application/ReactionService.java +++ b/src/main/java/com/depromeet/domain/reaction/application/ReactionService.java @@ -34,6 +34,7 @@ public class ReactionService { private final ReactionRepository reactionRepository; private final FcmService fcmService; + // TODO: ReactionGroupByEmojiResponse의 groupByEmoji 메서드를 사용하도록 변경 public List findAllReaction(Long missionRecordId) { Map> reactionMap = reactionRepository.findAllGroupByEmoji(missionRecordId); diff --git a/src/main/java/com/depromeet/domain/reaction/dto/response/ReactionGroupByEmojiResponse.java b/src/main/java/com/depromeet/domain/reaction/dto/response/ReactionGroupByEmojiResponse.java index e313218fb..6610e8d1e 100644 --- a/src/main/java/com/depromeet/domain/reaction/dto/response/ReactionGroupByEmojiResponse.java +++ b/src/main/java/com/depromeet/domain/reaction/dto/response/ReactionGroupByEmojiResponse.java @@ -1,11 +1,14 @@ package com.depromeet.domain.reaction.dto.response; +import static java.util.Comparator.*; + import com.depromeet.domain.member.dto.MemberProfileDto; import com.depromeet.domain.reaction.domain.EmojiType; import com.depromeet.domain.reaction.domain.Reaction; import java.time.LocalDateTime; import java.util.List; import java.util.Map; +import java.util.stream.Collectors; public record ReactionGroupByEmojiResponse( EmojiType emojiType, Integer count, List reactions) { @@ -30,4 +33,15 @@ public static ReactionDetailDto from(Reaction reaction) { MemberProfileDto.from(reaction.getMember())); } } + + // TODO: 리액션 그룹 단위에 해당하는 DTO가 컬렉션 로직을 들고있는 것이 올바른지 고민 필요 + public static List groupByEmojiType(List reactions) { + return reactions.stream() + .collect(Collectors.groupingBy(Reaction::getEmojiType)) + .entrySet() + .stream() + .map(ReactionGroupByEmojiResponse::from) + .sorted(comparing(ReactionGroupByEmojiResponse::count).reversed()) + .toList(); + } } diff --git a/src/test/java/com/depromeet/domain/feed/application/FeedServiceTest.java b/src/test/java/com/depromeet/domain/feed/application/FeedServiceTest.java new file mode 100644 index 000000000..a772bd7bd --- /dev/null +++ b/src/test/java/com/depromeet/domain/feed/application/FeedServiceTest.java @@ -0,0 +1,311 @@ +package com.depromeet.domain.feed.application; + +import static org.assertj.core.api.Assertions.*; + +import com.depromeet.DatabaseCleaner; +import com.depromeet.domain.comment.dao.CommentRepository; +import com.depromeet.domain.comment.domain.Comment; +import com.depromeet.domain.feed.domain.FeedVisibility; +import com.depromeet.domain.feed.dto.response.FeedOneResponse; +import com.depromeet.domain.follow.dao.MemberRelationRepository; +import com.depromeet.domain.follow.domain.MemberRelation; +import com.depromeet.domain.member.dao.MemberRepository; +import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.member.domain.OauthInfo; +import com.depromeet.domain.mission.dao.MissionRepository; +import com.depromeet.domain.mission.domain.Mission; +import com.depromeet.domain.mission.domain.MissionCategory; +import com.depromeet.domain.mission.domain.MissionVisibility; +import com.depromeet.domain.missionRecord.dao.MissionRecordRepository; +import com.depromeet.domain.missionRecord.domain.MissionRecord; +import com.depromeet.domain.reaction.dao.ReactionRepository; +import com.depromeet.domain.reaction.domain.EmojiType; +import com.depromeet.domain.reaction.domain.Reaction; +import com.depromeet.global.security.PrincipalDetails; +import java.time.Duration; +import java.time.LocalDateTime; +import java.time.LocalTime; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +class FeedServiceTest { + + @Autowired private FeedService feedService; + @Autowired private DatabaseCleaner databaseCleaner; + @Autowired private MemberRepository memberRepository; + @Autowired private MissionRepository missionRepository; + @Autowired private MissionRecordRepository missionRecordRepository; + @Autowired private ReactionRepository reactionRepository; + @Autowired private CommentRepository commentRepository; + @Autowired private MemberRelationRepository memberRelationRepository; + + @BeforeEach + void setUp() { + databaseCleaner.execute(); + } + + private void logoutAndReloginAs(Long memberId) { + SecurityContextHolder.clearContext(); // 현재 회원 로그아웃 + PrincipalDetails principalDetails = new PrincipalDetails(memberId, "USER"); + Authentication authentication = + new UsernamePasswordAuthenticationToken( + principalDetails, null, principalDetails.getAuthorities()); + SecurityContextHolder.getContext().setAuthentication(authentication); + } + + private void setFixture() { + Member member1 = + Member.createNormalMember( + OauthInfo.createOauthInfo("test1", "test1", "test1"), "test1"); + Member member2 = + Member.createNormalMember( + OauthInfo.createOauthInfo("test2", "test2", "test2"), "test2"); + Member member3 = + Member.createNormalMember( + OauthInfo.createOauthInfo("test3", "test3", "test3"), "test3"); + + memberRepository.saveAll(List.of(member1, member2, member3)); + + Mission mission1 = + Mission.createMission( + "mission1", + "content1", + 1, + MissionCategory.PROJECT, + MissionVisibility.ALL, + LocalDateTime.of(2024, 3, 1, 0, 0), + LocalDateTime.of(2024, 3, 14, 0, 0), + LocalTime.of(12, 0, 0), + member1); + + Mission mission2 = + Mission.createMission( + "mission2", + "content2", + 2, + MissionCategory.EXERCISE, + MissionVisibility.ALL, + LocalDateTime.of(2024, 3, 2, 0, 0), + LocalDateTime.of(2024, 3, 15, 0, 0), + LocalTime.of(12, 0, 0), + member2); + + Mission mission3 = + Mission.createMission( + "mission3", + "content3", + 3, + MissionCategory.STUDY, + MissionVisibility.FOLLOWER, + LocalDateTime.of(2024, 3, 3, 0, 0), + LocalDateTime.of(2024, 3, 16, 0, 0), + LocalTime.of(12, 0, 0), + member3); + + Mission mission4 = + Mission.createMission( + "mission4", + "content4", + 4, + MissionCategory.ETC, + MissionVisibility.NONE, + LocalDateTime.of(2024, 3, 4, 0, 0), + LocalDateTime.of(2024, 3, 17, 0, 0), + LocalTime.of(12, 0, 0), + member3); + + missionRepository.saveAll(List.of(mission1, mission2, mission3, mission4)); + + MissionRecord missionRecord1 = + MissionRecord.createMissionRecord( + Duration.ofMinutes(15), + LocalDateTime.of(2024, 3, 1, 12, 0), + LocalDateTime.of(2024, 3, 1, 12, 15), + mission1); + + MissionRecord missionRecord2 = + MissionRecord.createMissionRecord( + Duration.ofMinutes(25), + LocalDateTime.of(2024, 3, 2, 12, 0), + LocalDateTime.of(2024, 3, 2, 12, 25), + mission2); + + MissionRecord missionRecord3 = + MissionRecord.createMissionRecord( + Duration.ofMinutes(35), + LocalDateTime.of(2024, 3, 3, 12, 0), + LocalDateTime.of(2024, 3, 3, 12, 35), + mission3); + + MissionRecord missionRecord4 = + MissionRecord.createMissionRecord( + Duration.ofMinutes(45), + LocalDateTime.of(2024, 3, 4, 12, 0), + LocalDateTime.of(2024, 3, 4, 12, 45), + mission4); + + missionRecordRepository.saveAll( + List.of(missionRecord1, missionRecord2, missionRecord3, missionRecord4)); + + Reaction reactionToRecord1ByMember2 = + Reaction.createReaction(EmojiType.BLUE_HEART, member2, missionRecord1); + Reaction reactionToRecord1ByMember3 = + Reaction.createReaction(EmojiType.THUMBS_UP, member3, missionRecord1); + + Reaction reactionToRecord2ByMember1 = + Reaction.createReaction(EmojiType.FIRE, member1, missionRecord2); + Reaction reactionToRecord2ByMember3 = + Reaction.createReaction(EmojiType.PARTY_POPPER, member3, missionRecord2); + + Reaction reactionToRecord3ByMember1 = + Reaction.createReaction(EmojiType.UNICORN, member1, missionRecord3); + Reaction reactionToRecord3ByMember3 = + Reaction.createReaction(EmojiType.PARTYING_FACE, member3, missionRecord3); + + reactionRepository.saveAll( + List.of( + reactionToRecord1ByMember2, + reactionToRecord1ByMember3, + reactionToRecord2ByMember1, + reactionToRecord2ByMember3, + reactionToRecord3ByMember1, + reactionToRecord3ByMember3)); + + Comment commentToRecord1ByMember1 = + Comment.createComment("commentToRecord1ByMember1", member1, missionRecord1); + Comment commentToRecord1ByMember2 = + Comment.createComment("commentToRecord1ByMember2", member2, missionRecord1); + Comment commentToRecord1ByMember3 = + Comment.createComment("commentToRecord1ByMember3", member3, missionRecord1); + + Comment commentToRecord2ByMember1 = + Comment.createComment("commentToRecord2ByMember1", member1, missionRecord2); + Comment commentToRecord2ByMember2 = + Comment.createComment("commentToRecord2ByMember2", member2, missionRecord2); + Comment commentToRecord2ByMember3 = + Comment.createComment("commentToRecord2ByMember3", member3, missionRecord2); + + Comment commentToRecord3ByMember1 = + Comment.createComment("commentToRecord3ByMember1", member1, missionRecord3); + Comment commentToRecord3ByMember2 = + Comment.createComment("commentToRecord3ByMember2", member2, missionRecord3); + Comment commentToRecord3ByMember3 = + Comment.createComment("commentToRecord3ByMember3", member3, missionRecord3); + + commentRepository.saveAll( + List.of( + commentToRecord1ByMember1, + commentToRecord1ByMember2, + commentToRecord1ByMember3, + commentToRecord2ByMember1, + commentToRecord2ByMember2, + commentToRecord2ByMember3, + commentToRecord3ByMember1, + commentToRecord3ByMember2, + commentToRecord3ByMember3)); + + MemberRelation follow1to2 = MemberRelation.createMemberRelation(member1, member2); + MemberRelation follow1to3 = MemberRelation.createMemberRelation(member1, member3); + + memberRelationRepository.saveAll(List.of(follow1to2, follow1to3)); + } + + @Test + void 전체_피드이면_모든_미션기록을_조회한다() { + // given + setFixture(); + + // when + Pageable pageable = PageRequest.of(0, 10); + Slice response = feedService.findFeedV2(FeedVisibility.ALL, pageable); + + // then + assertThat(response.getContent()).hasSize(4); + } + + @Nested + class 팔로잉_피드이면 { + + @Test + void 조회에_성공한다() { + // given + setFixture(); + logoutAndReloginAs(1L); + + // when + Pageable pageable = PageRequest.of(0, 10); + Slice response = + feedService.findFeedV2(FeedVisibility.FOLLOWING, pageable); + + // then + assertThat(response.getContent()).hasSize(2); + } + + @Test + void 자신의_미션은_조회하지_않는다() { + // given + setFixture(); + logoutAndReloginAs(1L); + + // when + Pageable pageable = PageRequest.of(0, 10); + Slice response = + feedService.findFeedV2(FeedVisibility.FOLLOWING, pageable); + + // then + assertThat(response.getContent()) + .noneMatch(feedOneResponse -> feedOneResponse.memberId() == 1L); + } + + @Test + void 공개_및_팔로워공개_미션의_미션기록만_조회한다() { + // given + setFixture(); + logoutAndReloginAs(1L); + + // when + Pageable pageable = PageRequest.of(0, 10); + Slice response = + feedService.findFeedV2(FeedVisibility.FOLLOWING, pageable); + + // then + // 2번 미션의 미션기록은 공개이므로 조회 + assertThat(response.getContent()) + .filteredOn(feedOneResponse -> feedOneResponse.missionId() == 2L) + .hasSize(1); + // 3번 미션의 미션기록은 팔로워 공개이므로 조회 + assertThat(response.getContent()) + .filteredOn(feedOneResponse -> feedOneResponse.missionId() == 3L) + .hasSize(1); + } + + @Test + void 비공개_미션의_미션기록은_조회하지_않는다() { + // given + setFixture(); + logoutAndReloginAs(1L); + + // when + Pageable pageable = PageRequest.of(0, 10); + Slice response = + feedService.findFeedV2(FeedVisibility.FOLLOWING, pageable); + + // then + assertThat(response.getContent()) + .noneMatch(feedOneResponse -> feedOneResponse.missionId() == 4L); + } + } +} diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index 915f6cf78..a13f2fde4 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -5,3 +5,8 @@ spring: datasource: url: jdbc:h2:mem:test;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false;MODE=MYSQL + + jpa: + properties: + hibernate: + default_batch_fetch_size: 1000 From e1a1f37876d276e138e1a8674f8d6295dd54d909 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Fri, 26 Apr 2024 11:32:40 +0900 Subject: [PATCH 7/9] =?UTF-8?q?test:=20missionService=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=BD=94=EB=93=9C=20Transactional=20=EC=82=AD?= =?UTF-8?q?=EC=A0=9C=20(#386)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * test: missionService 테스트 코드 Transactional 삭제 * fix: 미사용 EntityManager 삭제 * fix: 브라우저에서 쿠키 접근 허용 여부 --- src/main/java/com/depromeet/global/util/CookieUtil.java | 4 ++-- .../domain/mission/service/MissionServiceTest.java | 6 +----- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/depromeet/global/util/CookieUtil.java b/src/main/java/com/depromeet/global/util/CookieUtil.java index 399adc157..deb191e70 100644 --- a/src/main/java/com/depromeet/global/util/CookieUtil.java +++ b/src/main/java/com/depromeet/global/util/CookieUtil.java @@ -59,7 +59,7 @@ public HttpHeaders deleteTokenCookies() { .maxAge(0) .secure(true) .sameSite(sameSite) - .httpOnly(false) + .httpOnly(true) .build(); ResponseCookie refreshTokenCookie = @@ -68,7 +68,7 @@ public HttpHeaders deleteTokenCookies() { .maxAge(0) .secure(true) .sameSite(sameSite) - .httpOnly(false) + .httpOnly(true) .build(); HttpHeaders headers = new HttpHeaders(); diff --git a/src/test/java/com/depromeet/domain/mission/service/MissionServiceTest.java b/src/test/java/com/depromeet/domain/mission/service/MissionServiceTest.java index dac47f9d5..223e8cab9 100644 --- a/src/test/java/com/depromeet/domain/mission/service/MissionServiceTest.java +++ b/src/test/java/com/depromeet/domain/mission/service/MissionServiceTest.java @@ -21,7 +21,6 @@ import com.depromeet.domain.mission.dto.response.MissionUpdateResponse; import com.depromeet.global.security.PrincipalDetails; import com.depromeet.global.util.MemberUtil; -import jakarta.persistence.EntityManager; import java.time.LocalDateTime; import java.time.LocalTime; import java.util.List; @@ -35,7 +34,6 @@ import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.test.context.ActiveProfiles; -import org.springframework.transaction.annotation.Transactional; @SpringBootTest @ActiveProfiles("test") @@ -45,7 +43,6 @@ class MissionServiceTest { @Autowired private MissionRepository missionRepository; @Autowired private MemberRepository memberRepository; @Autowired private DatabaseCleaner databaseCleaner; - @Autowired private EntityManager entityManager; @Autowired private MemberUtil memberUtil; @BeforeEach @@ -108,7 +105,6 @@ void setUp() { } @Test - @Transactional void 미션_리스트를_조회한다() { // given LocalDateTime startedAt = LocalDateTime.now(); @@ -125,7 +121,7 @@ void setUp() { LocalTime.of(21, 0))) .forEach( request -> - entityManager.persist( + missionRepository.save( Mission.createMission( request.name(), request.content(), From 24c6c21d31d06b878cafa21337c1e8e9bd749129 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=AA=A8?= Date: Thu, 9 May 2024 23:09:07 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=EC=95=8C=EB=A6=BC=EC=9D=84=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=ED=95=A0=20=EC=88=98=20=EC=9E=88=EB=8A=94?= =?UTF-8?q?=20=EC=95=8C=EB=A6=BC=EC=84=BC=ED=84=B0=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#392)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 알림을 확인 할 수 있는 알림센터 기능 추가 * fix: 쿼리 정렬조건 추가 --- ...roller.java => NotificationController.java} | 18 +++++++++++++----- ...shService.java => NotificationService.java} | 18 +++++++++++++++++- .../dao/NotificationRepository.java | 3 +++ .../dto/NotificationFindAllResponse.java | 16 ++++++++++++++++ ...eTest.java => NotificationServiceTest.java} | 16 ++++++++-------- 5 files changed, 57 insertions(+), 14 deletions(-) rename src/main/java/com/depromeet/domain/notification/api/{PushController.java => NotificationController.java} (63%) rename src/main/java/com/depromeet/domain/notification/application/{PushService.java => NotificationService.java} (83%) create mode 100644 src/main/java/com/depromeet/domain/notification/dto/NotificationFindAllResponse.java rename src/test/java/com/depromeet/domain/notification/application/{PushServiceTest.java => NotificationServiceTest.java} (95%) diff --git a/src/main/java/com/depromeet/domain/notification/api/PushController.java b/src/main/java/com/depromeet/domain/notification/api/NotificationController.java similarity index 63% rename from src/main/java/com/depromeet/domain/notification/api/PushController.java rename to src/main/java/com/depromeet/domain/notification/api/NotificationController.java index 9b197110f..124457584 100644 --- a/src/main/java/com/depromeet/domain/notification/api/PushController.java +++ b/src/main/java/com/depromeet/domain/notification/api/NotificationController.java @@ -1,11 +1,13 @@ package com.depromeet.domain.notification.api; -import com.depromeet.domain.notification.application.PushService; +import com.depromeet.domain.notification.application.NotificationService; +import com.depromeet.domain.notification.dto.NotificationFindAllResponse; import com.depromeet.domain.notification.dto.request.PushMissionRemindRequest; import com.depromeet.domain.notification.dto.request.PushUrgingSendRequest; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; +import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; @@ -13,18 +15,24 @@ @RestController @RequestMapping("/notifications") @RequiredArgsConstructor -public class PushController { - private final PushService pushService; +public class NotificationController { + private final NotificationService notificationService; + + @Operation(summary = "알림센터 조회", description = "알림센터 목록을 조회합니다.") + @GetMapping + public List notificationFindAll() { + return notificationService.findAllNotification(); + } @Operation(summary = "재촉하기", description = "당일 미션을 완료하지 않은 친구에게 재촉하기 Push Message를 발송합니다.") @PostMapping("/urging") public void urgingSend(@Valid @RequestBody PushUrgingSendRequest request) { - pushService.sendUrgingPush(request); + notificationService.sendUrgingPush(request); } @Operation(summary = "미션 타이머 리마인드 알림", description = "인증을 놓치는 경우에 대비하여 리마인드 알림을 전송합니다.") @PostMapping("/missions/remind") public void missionRemindSend(@Valid @RequestBody PushMissionRemindRequest request) { - pushService.sendMissionRemindPush(request); + notificationService.sendMissionRemindPush(request); } } diff --git a/src/main/java/com/depromeet/domain/notification/application/PushService.java b/src/main/java/com/depromeet/domain/notification/application/NotificationService.java similarity index 83% rename from src/main/java/com/depromeet/domain/notification/application/PushService.java rename to src/main/java/com/depromeet/domain/notification/application/NotificationService.java index 33e763498..f73ad4b53 100644 --- a/src/main/java/com/depromeet/domain/notification/application/PushService.java +++ b/src/main/java/com/depromeet/domain/notification/application/NotificationService.java @@ -8,6 +8,7 @@ import com.depromeet.domain.notification.dao.NotificationRepository; import com.depromeet.domain.notification.domain.Notification; import com.depromeet.domain.notification.domain.NotificationType; +import com.depromeet.domain.notification.dto.NotificationFindAllResponse; import com.depromeet.domain.notification.dto.request.PushMissionRemindRequest; import com.depromeet.domain.notification.dto.request.PushUrgingSendRequest; import com.depromeet.global.common.constants.PushNotificationConstants; @@ -15,6 +16,8 @@ import com.depromeet.global.error.exception.ErrorCode; import com.depromeet.global.util.MemberUtil; import java.time.Instant; +import java.util.List; +import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; import org.springframework.scheduling.TaskScheduler; import org.springframework.stereotype.Service; @@ -23,7 +26,7 @@ @Service @RequiredArgsConstructor @Transactional -public class PushService { +public class NotificationService { private final MemberUtil memberUtil; private final FcmService fcmService; private final MissionRepository missionRepository; @@ -68,6 +71,19 @@ public void sendMissionRemindPush(PushMissionRemindRequest request) { Instant.now().plusSeconds(request.seconds())); } + public List findAllNotification() { + final Member member = memberUtil.getCurrentMember(); + return notificationRepository + .findAllByTargetMemberIdOrderByCreatedAtDesc(member.getId()) + .stream() + .map( + notification -> + NotificationFindAllResponse.of( + notification.getNotificationType(), + notification.getCreatedAt())) + .collect(Collectors.toList()); + } + private void validateFinishedMission(Mission mission) { if (mission.isFinished()) { throw new CustomException(ErrorCode.FINISHED_MISSION_URGING_NOT_ALLOWED); diff --git a/src/main/java/com/depromeet/domain/notification/dao/NotificationRepository.java b/src/main/java/com/depromeet/domain/notification/dao/NotificationRepository.java index 0b70f4801..beddf1e0b 100644 --- a/src/main/java/com/depromeet/domain/notification/dao/NotificationRepository.java +++ b/src/main/java/com/depromeet/domain/notification/dao/NotificationRepository.java @@ -2,10 +2,13 @@ import com.depromeet.domain.notification.domain.Notification; import com.depromeet.domain.notification.domain.NotificationType; +import java.util.List; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; public interface NotificationRepository extends JpaRepository { Optional findBySourceMemberIdAndTargetMemberIdAndNotificationType( Long sourceId, Long targetId, NotificationType notificationType); + + List findAllByTargetMemberIdOrderByCreatedAtDesc(Long targetMemberId); } diff --git a/src/main/java/com/depromeet/domain/notification/dto/NotificationFindAllResponse.java b/src/main/java/com/depromeet/domain/notification/dto/NotificationFindAllResponse.java new file mode 100644 index 000000000..ffac7c406 --- /dev/null +++ b/src/main/java/com/depromeet/domain/notification/dto/NotificationFindAllResponse.java @@ -0,0 +1,16 @@ +package com.depromeet.domain.notification.dto; + +import com.depromeet.domain.mission.domain.*; +import com.depromeet.domain.notification.domain.NotificationType; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +public record NotificationFindAllResponse( + @Schema(description = "알림 타입") NotificationType notificationType, + @Schema(description = "알림 날짜") LocalDateTime createdAt) { + + public static NotificationFindAllResponse of( + NotificationType notificationType, LocalDateTime createdAt) { + return new NotificationFindAllResponse(notificationType, createdAt); + } +} diff --git a/src/test/java/com/depromeet/domain/notification/application/PushServiceTest.java b/src/test/java/com/depromeet/domain/notification/application/NotificationServiceTest.java similarity index 95% rename from src/test/java/com/depromeet/domain/notification/application/PushServiceTest.java rename to src/test/java/com/depromeet/domain/notification/application/NotificationServiceTest.java index 82150bf67..0492f9783 100644 --- a/src/test/java/com/depromeet/domain/notification/application/PushServiceTest.java +++ b/src/test/java/com/depromeet/domain/notification/application/NotificationServiceTest.java @@ -41,7 +41,7 @@ @SpringBootTest @ActiveProfiles("test") -class PushServiceTest { +class NotificationServiceTest { @Autowired private DatabaseCleaner databaseCleaner; @Autowired private MemberUtil memberUtil; @@ -56,7 +56,7 @@ class PushServiceTest { @Autowired private MissionRecordRepository missionRecordRepository; - @Autowired private PushService pushService; + @Autowired private NotificationService notificationService; @BeforeEach void setUp() { @@ -76,7 +76,7 @@ class 친구에게_미션을_재촉할_때 { PushUrgingSendRequest request = new PushUrgingSendRequest(1L); // when, then - assertThatThrownBy(() -> pushService.sendUrgingPush(request)) + assertThatThrownBy(() -> notificationService.sendUrgingPush(request)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.MEMBER_NOT_FOUND.getMessage()); } @@ -90,7 +90,7 @@ class 친구에게_미션을_재촉할_때 { Profile.createProfile("testNickname1", "testImageUrl1"))); // when, then - assertThatThrownBy(() -> pushService.sendUrgingPush(request)) + assertThatThrownBy(() -> notificationService.sendUrgingPush(request)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.MISSION_NOT_FOUND.getMessage()); } @@ -119,7 +119,7 @@ class 친구에게_미션을_재촉할_때 { currentMember)); // when, then - assertThatThrownBy(() -> pushService.sendUrgingPush(request)) + assertThatThrownBy(() -> notificationService.sendUrgingPush(request)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.SELF_SENDING_NOT_ALLOWED.getMessage()); } @@ -152,7 +152,7 @@ class 친구에게_미션을_재촉할_때 { targetMember)); // when, then - assertThatThrownBy(() -> pushService.sendUrgingPush(request)) + assertThatThrownBy(() -> notificationService.sendUrgingPush(request)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.FINISHED_MISSION_URGING_NOT_ALLOWED.getMessage()); } @@ -202,7 +202,7 @@ class 친구에게_미션을_재촉할_때 { missionRecordRepository.save(missionRecord); // when, then - assertThatThrownBy(() -> pushService.sendUrgingPush(request)) + assertThatThrownBy(() -> notificationService.sendUrgingPush(request)) .isInstanceOf(CustomException.class) .hasMessage(ErrorCode.TODAY_COMPLETED_MISSION_SENDING_NOT_ALLOWED.getMessage()); } @@ -240,7 +240,7 @@ class 친구에게_미션을_재촉할_때 { targetMember)); // when - pushService.sendUrgingPush(request); + notificationService.sendUrgingPush(request); // then Optional optionalNotification = notificationRepository.findById(1L); From fd3649a68ea1293df9b23eab130ad7f45d061caf Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Thu, 16 May 2024 01:33:01 +0900 Subject: [PATCH 9/9] =?UTF-8?q?chore:=20Object=20Storage=20->=20AWS=20S3?= =?UTF-8?q?=20Bucket=20=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD=20(#395)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * chore: Object Storage -> AWS S3 Bucket 으로 변경 * chore: AWS_S3 -> S3 --- .../domain/image/application/ImageService.java | 12 ++++++------ .../infra/config/properties/PropertiesConfig.java | 4 ++-- .../{storage/StorageConfig.java => s3/S3Config.java} | 11 +++++------ .../StorageProperties.java => s3/S3Properties.java} | 4 ++-- src/main/resources/application-storage.yml | 10 +++++----- 5 files changed, 20 insertions(+), 21 deletions(-) rename src/main/java/com/depromeet/infra/config/{storage/StorageConfig.java => s3/S3Config.java} (73%) rename src/main/java/com/depromeet/infra/config/{storage/StorageProperties.java => s3/S3Properties.java} (73%) diff --git a/src/main/java/com/depromeet/domain/image/application/ImageService.java b/src/main/java/com/depromeet/domain/image/application/ImageService.java index 9f771f66a..bbe6daa74 100644 --- a/src/main/java/com/depromeet/domain/image/application/ImageService.java +++ b/src/main/java/com/depromeet/domain/image/application/ImageService.java @@ -25,7 +25,7 @@ import com.depromeet.global.error.exception.ErrorCode; import com.depromeet.global.util.MemberUtil; import com.depromeet.global.util.SpringEnvironmentUtil; -import com.depromeet.infra.config.storage.StorageProperties; +import com.depromeet.infra.config.s3.S3Properties; import java.util.Date; import java.util.UUID; import lombok.RequiredArgsConstructor; @@ -38,7 +38,7 @@ public class ImageService { private final MemberUtil memberUtil; private final SpringEnvironmentUtil springEnvironmentUtil; - private final StorageProperties storageProperties; + private final S3Properties s3Properties; private final AmazonS3 amazonS3; private final MissionRecordRepository missionRecordRepository; private final MissionRecordTtlRepository missionRecordTtlRepository; @@ -62,7 +62,7 @@ public PresignedUrlResponse createMissionRecordPresignedUrl( request.imageFileExtension()); GeneratePresignedUrlRequest generatePresignedUrlRequest = createGeneratePreSignedUrlRequest( - storageProperties.bucket(), + s3Properties.bucket(), fileName, request.imageFileExtension().getUploadExtension()); @@ -113,7 +113,7 @@ public PresignedUrlResponse createMemberProfilePresignedUrl( request.imageFileExtension()); GeneratePresignedUrlRequest generatePresignedUrlRequest = createGeneratePreSignedUrlRequest( - storageProperties.bucket(), + s3Properties.bucket(), fileName, request.imageFileExtension().getUploadExtension()); @@ -205,9 +205,9 @@ private String createUploadImageUrl( Long targetId, String imageKey, ImageFileExtension imageFileExtension) { - return storageProperties.endpoint() + return s3Properties.endpoint() + "/" - + storageProperties.bucket() + + s3Properties.bucket() + "/" + springEnvironmentUtil.getCurrentProfile() + "/" diff --git a/src/main/java/com/depromeet/infra/config/properties/PropertiesConfig.java b/src/main/java/com/depromeet/infra/config/properties/PropertiesConfig.java index c94e19f16..dfbb85699 100644 --- a/src/main/java/com/depromeet/infra/config/properties/PropertiesConfig.java +++ b/src/main/java/com/depromeet/infra/config/properties/PropertiesConfig.java @@ -3,12 +3,12 @@ import com.depromeet.infra.config.jwt.JwtProperties; import com.depromeet.infra.config.oidc.OidcProperties; import com.depromeet.infra.config.redis.RedisProperties; -import com.depromeet.infra.config.storage.StorageProperties; +import com.depromeet.infra.config.s3.S3Properties; import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.context.annotation.Configuration; @EnableConfigurationProperties({ - StorageProperties.class, + S3Properties.class, RedisProperties.class, JwtProperties.class, OidcProperties.class diff --git a/src/main/java/com/depromeet/infra/config/storage/StorageConfig.java b/src/main/java/com/depromeet/infra/config/s3/S3Config.java similarity index 73% rename from src/main/java/com/depromeet/infra/config/storage/StorageConfig.java rename to src/main/java/com/depromeet/infra/config/s3/S3Config.java index 29b549694..98e8a6823 100644 --- a/src/main/java/com/depromeet/infra/config/storage/StorageConfig.java +++ b/src/main/java/com/depromeet/infra/config/s3/S3Config.java @@ -1,4 +1,4 @@ -package com.depromeet.infra.config.storage; +package com.depromeet.infra.config.s3; import com.amazonaws.auth.AWSCredentials; import com.amazonaws.auth.AWSStaticCredentialsProvider; @@ -12,17 +12,16 @@ @Configuration @RequiredArgsConstructor -public class StorageConfig { - private final StorageProperties storageProperties; +public class S3Config { + private final S3Properties s3Properties; @Bean public AmazonS3 amazonS3() { AWSCredentials credentials = - new BasicAWSCredentials( - storageProperties.accessKey(), storageProperties.secretKey()); + new BasicAWSCredentials(s3Properties.accessKey(), s3Properties.secretKey()); AwsClientBuilder.EndpointConfiguration endpointConfiguration = new AwsClientBuilder.EndpointConfiguration( - storageProperties.endpoint(), storageProperties.region()); + s3Properties.endpoint(), s3Properties.region()); return AmazonS3ClientBuilder.standard() .withEndpointConfiguration(endpointConfiguration) diff --git a/src/main/java/com/depromeet/infra/config/storage/StorageProperties.java b/src/main/java/com/depromeet/infra/config/s3/S3Properties.java similarity index 73% rename from src/main/java/com/depromeet/infra/config/storage/StorageProperties.java rename to src/main/java/com/depromeet/infra/config/s3/S3Properties.java index 229e6fa82..ef15697f6 100644 --- a/src/main/java/com/depromeet/infra/config/storage/StorageProperties.java +++ b/src/main/java/com/depromeet/infra/config/s3/S3Properties.java @@ -1,7 +1,7 @@ -package com.depromeet.infra.config.storage; +package com.depromeet.infra.config.s3; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "storage") -public record StorageProperties( +public record S3Properties( String accessKey, String secretKey, String region, String bucket, String endpoint) {} diff --git a/src/main/resources/application-storage.yml b/src/main/resources/application-storage.yml index eb57092c4..7556a10a7 100644 --- a/src/main/resources/application-storage.yml +++ b/src/main/resources/application-storage.yml @@ -3,8 +3,8 @@ spring: activate: on-profile: "storage" storage: - accessKey: ${STORAGE_ACCESS_KEY:} - secretKey: ${STORAGE_SECRET_KEY:} - bucket: ${STORAGE_BUCKET:} - region: ${STORAGE_REGION:} - endpoint: ${STORAGE_ENDPOINT:https://kr.object.ncloudstorage.com} + accessKey: ${AWS_ACCESS_KEY:} + secretKey: ${AWS_SECRET_KEY:} + bucket: ${S3_BUCKET:} + region: ${AWS_REGION:} + endpoint: ${S3_ENDPOINT:https://s3.ap-northeast-2.amazonaws.com}