From 0fe363488cdacb474d76437d891f65fc0b9b83b4 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Tue, 6 Feb 2024 23:43:22 -0800 Subject: [PATCH 1/8] =?UTF-8?q?feat:=20=ED=94=BC=EB=93=9C=20=ED=83=AD=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84=20(#277)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: feed 탭 리스트 구현 * feat: 프로필 피드 탭 구현 * feat: 피드 탭 닉네임 추가 * feat: 피드 탭 v2 * fix: 마이프로필 피드 공개 여부 예외 처리 * refactor: FeedOneResponse class -> record * refactor: feed/me API 개선 * refactor: 피드 탭 리스트 limit 100 제한 * fix: 리뷰 반영 * fix: targetId -> memberId * fix: 타인 프로필 공개여부 조건 추가 * fix: Transactional 추가 * fix: getter id가 아닌 member 프록시 객체 리스트로 수정 * fix: @uwoobeat 리뷰 반영 * fix: 주석 삭제 --- .../domain/feed/api/FeedController.java | 34 +++++++ .../domain/feed/application/FeedService.java | 92 ++++++++++++++++++ .../response/FeedOneByProfileResponse.java | 49 ++++++++++ .../feed/dto/response/FeedOneResponse.java | 96 +++++++++++++++++++ .../follow/application/FollowService.java | 1 + .../dao/MemberRelationRepositoryImpl.java | 1 + .../mission/dao/MissionRepositoryImpl.java | 1 + .../dao/MissionRecordRepositoryCustom.java | 7 ++ .../dao/MissionRecordRepositoryImpl.java | 55 +++++++++++ src/main/resources/application-local.yml | 1 + 10 files changed, 337 insertions(+) create mode 100644 src/main/java/com/depromeet/domain/feed/api/FeedController.java create mode 100644 src/main/java/com/depromeet/domain/feed/application/FeedService.java create mode 100644 src/main/java/com/depromeet/domain/feed/dto/response/FeedOneByProfileResponse.java create mode 100644 src/main/java/com/depromeet/domain/feed/dto/response/FeedOneResponse.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 new file mode 100644 index 000000000..8d802e43b --- /dev/null +++ b/src/main/java/com/depromeet/domain/feed/api/FeedController.java @@ -0,0 +1,34 @@ +package com.depromeet.domain.feed.api; + +import com.depromeet.domain.feed.application.FeedService; +import com.depromeet.domain.feed.dto.response.FeedOneByProfileResponse; +import com.depromeet.domain.feed.dto.response.FeedOneResponse; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +@RestController +@Tag(name = "6. [피드]", description = "피드 관련 API입니다.") +@RequestMapping("/feed") +@RequiredArgsConstructor +public class FeedController { + + private final FeedService feedService; + + @Operation(summary = "피드 탭", description = "피드 탭을 조회합니다.") + @GetMapping("/me") + public List feedFindAll() { + return feedService.findAllFeed(); + } + + @Operation(summary = "프로필 피드", description = "피드 탭을 조회합니다.") + @GetMapping("/{memberId}") + public List feedFindAllByTargetId(@PathVariable Long memberId) { + return feedService.findAllFeedByTargetId(memberId); + } +} diff --git a/src/main/java/com/depromeet/domain/feed/application/FeedService.java b/src/main/java/com/depromeet/domain/feed/application/FeedService.java new file mode 100644 index 000000000..5600253cc --- /dev/null +++ b/src/main/java/com/depromeet/domain/feed/application/FeedService.java @@ -0,0 +1,92 @@ +package com.depromeet.domain.feed.application; + +import com.depromeet.domain.feed.dto.response.FeedOneByProfileResponse; +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.domain.Member; +import com.depromeet.domain.mission.domain.MissionVisibility; +import com.depromeet.domain.missionRecord.dao.MissionRecordRepository; +import com.depromeet.domain.missionRecord.domain.MissionRecord; +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.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +// TODO: Redis 사용해서 캐싱 작업 필요 +@Service +@RequiredArgsConstructor +@Transactional +public class FeedService { + private final MemberUtil memberUtil; + private final MissionRecordRepository missionRecordRepository; + private final MemberRelationRepository memberRelationRepository; + private final SecurityUtil securityUtil; + + @Transactional(readOnly = true) + public List findAllFeed() { + final Member currentMember = memberUtil.getCurrentMember(); + List members = + memberRelationRepository.findAllBySourceId(currentMember.getId()).stream() + .map(MemberRelation::getTarget) + .collect(Collectors.toList()); + members.add(currentMember); + + return missionRecordRepository.findFeedAll(members); + } + + @Transactional(readOnly = true) + public List findAllFeedByTargetId(Long targetId) { + final Long sourceId = securityUtil.getCurrentMemberId(); + + if (isMyFeedRequired(targetId, sourceId)) { + return findFeedByOtherMember(sourceId, targetId); + } + return findFeedByCurrentMember(sourceId); + } + + private boolean isMyFeedRequired(Long targetId, Long sourceId) { + return !targetId.equals(sourceId); + } + + private List findFeedByOtherMember(Long sourceId, Long targetId) { + final Member targetMember = memberUtil.getMemberByMemberId(targetId); + + // 팔로우 관계 true: visibility.FOLLOW and ALL, false: visibility.ALL only + boolean isMemberRelationExistsWithMe = + memberRelationRepository.existsBySourceIdAndTargetId( + sourceId, targetMember.getId()); + List visibilities = + determineVisibilityConditionsByRelationsWithMe(isMemberRelationExistsWithMe); + List feedAllByMemberId = + missionRecordRepository.findFeedAllByMemberId(targetId, visibilities); + return extractFeedResponses(feedAllByMemberId); + } + + private List findFeedByCurrentMember(Long sourceId) { + List visibilities = + List.of(MissionVisibility.NONE, MissionVisibility.FOLLOWER, MissionVisibility.ALL); + List feedAllByMemberId = + missionRecordRepository.findFeedAllByMemberId(sourceId, visibilities); + return extractFeedResponses(feedAllByMemberId); + } + + private List extractFeedResponses(List records) { + return records.stream().map(FeedOneByProfileResponse::of).toList(); + } + + private List determineVisibilityConditionsByRelationsWithMe( + boolean isMemberRelationExistsWithMe) { + List visibilities = new ArrayList<>(); + visibilities.add(MissionVisibility.ALL); + + if (isMemberRelationExistsWithMe) { + visibilities.add(MissionVisibility.FOLLOWER); + } + return visibilities; + } +} 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 new file mode 100644 index 000000000..16577d97f --- /dev/null +++ b/src/main/java/com/depromeet/domain/feed/dto/response/FeedOneByProfileResponse.java @@ -0,0 +1,49 @@ +package com.depromeet.domain.feed.dto.response; + +import com.depromeet.domain.missionRecord.domain.MissionRecord; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.Duration; +import java.time.LocalDateTime; + +public record FeedOneByProfileResponse( + @Schema(description = "미션 ID", defaultValue = "1") Long missionId, + @Schema(description = "미션 기록 ID", defaultValue = "1") Long recordId, + @Schema(description = "미션 이름", defaultValue = "default name") String name, + @Schema( + description = "미션 기록 인증 사진 Url", + defaultValue = "https://image.10mm.today/default.png") + String recordImageUrl, + @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 = "미션 기록 시작 시간", + defaultValue = "2023-01-06 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-20 00:34:00", + type = "string") + LocalDateTime finishedAt) { + + public static FeedOneByProfileResponse of(MissionRecord record) { + return new FeedOneByProfileResponse( + record.getMission().getId(), + record.getId(), + record.getMission().getName(), + record.getImageUrl(), + record.getDuration().toMinutes(), + Duration.between(record.getStartedAt(), LocalDateTime.now()).toDays() + 1, + record.getStartedAt(), + record.getFinishedAt()); + } +} 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 new file mode 100644 index 000000000..9cf071b9a --- /dev/null +++ b/src/main/java/com/depromeet/domain/feed/dto/response/FeedOneResponse.java @@ -0,0 +1,96 @@ +package com.depromeet.domain.feed.dto.response; + +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; + +public record FeedOneResponse( + @Schema(description = "작성자 ID", defaultValue = "1") Long memberId, + @Schema(description = "작성자 닉네임", defaultValue = "default name") String nickname, + @Schema(description = "작성자 프로필 이미지", defaultValue = "https://image.10mm.today/default.png") + String profileImage, + @Schema(description = "미션 ID", defaultValue = "1") Long missionId, + @Schema(description = "미션 이름", defaultValue = "default name") String name, + @Schema(description = "미션 기록 ID", defaultValue = "1") Long recordId, + @Schema(description = "미션 일지 내용", defaultValue = "default remark") String remark, + @Schema( + description = "미션 기록 인증 사진 Url", + defaultValue = "https://image.10mm.today/default.png") + String recordImageUrl, + @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 = "미션 기록 시작 시간", + defaultValue = "2024-01-06 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-20 00:34:00", + type = "string") + LocalDateTime finishedAt) { + @QueryProjection + public FeedOneResponse( + Long memberId, + String nickname, + String profileImage, + Long missionId, + String name, + Long recordId, + String remark, + String recordImageUrl, + Duration duration, + LocalDateTime startedAt, + LocalDateTime finishedAt) { + this( + memberId, + nickname, + profileImage, + missionId, + name, + recordId, + remark, + recordImageUrl, + duration.toMinutes(), + Duration.between(startedAt, LocalDateTime.now()).toDays() + 1, + startedAt, + finishedAt); + } + + 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) { + return new FeedOneResponse( + memberId, + nickname, + profileImage, + missionId, + name, + recordId, + remark, + recordImageUrl, + duration.toMinutes(), + Duration.between(startedAt, LocalDateTime.now()).toDays() + 1, + startedAt, + finishedAt); + } +} 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 3cb37811b..32a77a931 100644 --- a/src/main/java/com/depromeet/domain/follow/application/FollowService.java +++ b/src/main/java/com/depromeet/domain/follow/application/FollowService.java @@ -187,6 +187,7 @@ private Member getTargetMember(Long targetId) { return targetMember; } + @Transactional(readOnly = true) public FollowListResponse findFollowList(Long targetId) { final Member currentMember = memberUtil.getCurrentMember(); Member targetMember = getTargetMember(targetId); diff --git a/src/main/java/com/depromeet/domain/follow/dao/MemberRelationRepositoryImpl.java b/src/main/java/com/depromeet/domain/follow/dao/MemberRelationRepositoryImpl.java index 76c856115..712b75994 100644 --- a/src/main/java/com/depromeet/domain/follow/dao/MemberRelationRepositoryImpl.java +++ b/src/main/java/com/depromeet/domain/follow/dao/MemberRelationRepositoryImpl.java @@ -22,6 +22,7 @@ public List findAllBySourceId(Long memberId) { return jpaQueryFactory .selectFrom(memberRelation) .leftJoin(memberRelation.target, member) + .fetchJoin() .leftJoin(memberRelation.target.missions, mission) .leftJoin(mission.missionRecords, missionRecord) .where(sourceIdEq(memberId)) 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 d51e8fac9..8eec6bf2d 100644 --- a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java +++ b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java @@ -1,5 +1,6 @@ package com.depromeet.domain.mission.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.*; 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 ee09b5e1e..03ce2853a 100644 --- a/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryCustom.java +++ b/src/main/java/com/depromeet/domain/missionRecord/dao/MissionRecordRepositoryCustom.java @@ -1,5 +1,8 @@ package com.depromeet.domain.missionRecord.dao; +import com.depromeet.domain.feed.dto.response.FeedOneResponse; +import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.mission.domain.MissionVisibility; import com.depromeet.domain.missionRecord.domain.MissionRecord; import java.time.YearMonth; import java.util.List; @@ -8,6 +11,10 @@ public interface MissionRecordRepositoryCustom { List findAllByMissionIdAndYearMonth(Long missionId, YearMonth yearMonth); + List findFeedAll(List members); + + List findFeedAllByMemberId(Long memberId, List visibilities); + boolean isCompletedMissionExistsToday(Long missionId); void deleteByMissionRecordId(Long missionRecordId); 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 498e8de8f..63d5d7338 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,15 @@ package com.depromeet.domain.missionRecord.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.feed.dto.response.FeedOneResponse; +import com.depromeet.domain.member.domain.Member; +import com.depromeet.domain.mission.domain.MissionVisibility; +import com.depromeet.domain.missionRecord.domain.ImageUploadStatus; import com.depromeet.domain.missionRecord.domain.MissionRecord; +import com.querydsl.core.types.Projections; import com.querydsl.core.types.dsl.BooleanExpression; import com.querydsl.jpa.impl.JPAQueryFactory; import java.time.LocalDate; @@ -16,6 +23,7 @@ public class MissionRecordRepositoryImpl implements MissionRecordRepositoryCustom { private final JPAQueryFactory jpaQueryFactory; + private static final long FEED_TAB_LIMIT = 100; @Override public List findAllByMissionIdAndYearMonth(Long missionId, YearMonth yearMonth) { @@ -44,6 +52,53 @@ public boolean isCompletedMissionExistsToday(Long missionId) { return missionRecordFetchOne != null; } + @Override + public List findFeedAll(List members) { + return jpaQueryFactory + .select( + Projections.constructor( + FeedOneResponse.class, + member.id, + member.profile.nickname, + member.profile.profileImageUrl, + mission.id, + mission.name, + missionRecord.id, + missionRecord.remark, + missionRecord.imageUrl, + missionRecord.duration, + missionRecord.startedAt, + missionRecord.finishedAt)) + .from(missionRecord) + .leftJoin(missionRecord.mission, mission) + .on(mission.id.eq(missionRecord.mission.id)) + .leftJoin(mission.member, member) + .on(mission.member.id.eq(missionRecord.mission.member.id)) + .where( + missionRecord.mission.member.in(members), + missionRecord.mission.visibility.in( + MissionVisibility.FOLLOWER, MissionVisibility.ALL), + missionRecord.uploadStatus.eq(ImageUploadStatus.COMPLETE)) + .orderBy(missionRecord.startedAt.desc()) + .limit(FEED_TAB_LIMIT) + .fetch(); + } + + @Override + public List findFeedAllByMemberId( + Long memberId, List visibilities) { + return jpaQueryFactory + .selectFrom(missionRecord) + .leftJoin(missionRecord.mission, mission) + .fetchJoin() + .where( + mission.visibility.in(visibilities), + mission.member.id.eq(memberId), + missionRecord.uploadStatus.eq(ImageUploadStatus.COMPLETE)) + .orderBy(missionRecord.startedAt.desc()) + .fetch(); + } + @Override public void deleteByMissionRecordId(Long missionRecordId) { jpaQueryFactory.delete(missionRecord).where(missionRecord.id.eq(missionRecordId)).execute(); diff --git a/src/main/resources/application-local.yml b/src/main/resources/application-local.yml index 8ffb6c953..3ed416967 100644 --- a/src/main/resources/application-local.yml +++ b/src/main/resources/application-local.yml @@ -15,3 +15,4 @@ logging: level: org.springframework.orm.jpa: DEBUG org.springframework.transaction: DEBUG + From 09a696606cb8f2f87aca0ce7a223a607bbe48b1b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=AA=A8?= Date: Wed, 7 Feb 2024 21:18:17 +0900 Subject: [PATCH 2/8] =?UTF-8?q?feat:=20=EB=82=B4=20=ED=8C=94=EB=A1=9C?= =?UTF-8?q?=EC=9B=8C=20=EB=AA=A9=EB=A1=9D=20=EC=A4=91=20=EC=84=A0=ED=83=9D?= =?UTF-8?q?=ED=95=9C=20=EC=9C=A0=EC=A0=80=20=EB=82=B4=20=ED=8C=94=EB=A1=9C?= =?UTF-8?q?=EC=9B=8C=EC=97=90=EC=84=9C=20=EC=A7=80=EC=9A=B0=EA=B8=B0=20(#2?= =?UTF-8?q?87)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 내 팔로워 목록 중 선택한 유저 내 팔로워에서 지우기 * test: 팔로워 삭제 테스트코드 작성 * fix: @uwoobeat 코드리뷰 수정 --- .../domain/follow/api/FollowController.java | 11 ++- .../follow/application/FollowService.java | 18 ++++ .../dto/response/FollowerDeletedResponse.java | 12 +++ .../follow/application/FollowServiceTest.java | 91 ++++++++++++++++++- 4 files changed, 124 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/depromeet/domain/follow/dto/response/FollowerDeletedResponse.java 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 afac7d44c..b85fe03bf 100644 --- a/src/main/java/com/depromeet/domain/follow/api/FollowController.java +++ b/src/main/java/com/depromeet/domain/follow/api/FollowController.java @@ -3,10 +3,7 @@ import com.depromeet.domain.follow.application.FollowService; import com.depromeet.domain.follow.dto.request.FollowCreateRequest; import com.depromeet.domain.follow.dto.request.FollowDeleteRequest; -import com.depromeet.domain.follow.dto.response.FollowFindMeInfoResponse; -import com.depromeet.domain.follow.dto.response.FollowFindTargetInfoResponse; -import com.depromeet.domain.follow.dto.response.FollowListResponse; -import com.depromeet.domain.follow.dto.response.MemberFollowedResponse; +import com.depromeet.domain.follow.dto.response.*; import io.swagger.v3.oas.annotations.Operation; import io.swagger.v3.oas.annotations.tags.Tag; import jakarta.validation.Valid; @@ -63,4 +60,10 @@ public List followedUserFindAll() { public FollowListResponse followList(@PathVariable Long targetId) { return followService.findFollowList(targetId); } + + @DeleteMapping({"/{targetId}"}) + @Operation(summary = "팔로워 삭제", description = "내 팔로워 목록 중 targetId로 팔로워를 삭제합니다.") + public ResponseEntity followerDelete(@PathVariable Long targetId) { + return ResponseEntity.ok(followService.deleteFollower(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 32a77a931..798346207 100644 --- a/src/main/java/com/depromeet/domain/follow/application/FollowService.java +++ b/src/main/java/com/depromeet/domain/follow/application/FollowService.java @@ -225,6 +225,24 @@ public FollowListResponse findFollowList(Long targetId) { targetMember.getProfile().getNickname(), followingList, followerList); } + public FollowerDeletedResponse deleteFollower(Long targetId) { + final Member currentMember = memberUtil.getCurrentMember(); + final Member targetMember = getTargetMember(targetId); + + MemberRelation memberRelation = + memberRelationRepository + .findBySourceIdAndTargetId(targetMember.getId(), currentMember.getId()) + .orElseThrow(() -> new CustomException(ErrorCode.FOLLOW_NOT_EXIST)); + memberRelationRepository.delete(memberRelation); + + Optional optionalMemberRelation = + memberRelationRepository.findBySourceIdAndTargetId( + currentMember.getId(), targetMember.getId()); + return optionalMemberRelation.isPresent() + ? FollowerDeletedResponse.from(FollowStatus.FOLLOWING) + : FollowerDeletedResponse.from(FollowStatus.NOT_FOLLOWING); + } + private static void getFollowStatusIncludeList( List targetMembers, List currentMemberSources, diff --git a/src/main/java/com/depromeet/domain/follow/dto/response/FollowerDeletedResponse.java b/src/main/java/com/depromeet/domain/follow/dto/response/FollowerDeletedResponse.java new file mode 100644 index 000000000..6373e551b --- /dev/null +++ b/src/main/java/com/depromeet/domain/follow/dto/response/FollowerDeletedResponse.java @@ -0,0 +1,12 @@ +package com.depromeet.domain.follow.dto.response; + +import io.swagger.v3.oas.annotations.media.Schema; + +public record FollowerDeletedResponse( + @Schema(description = "팔로워 삭제 후 나와 target과의 팔로우 상태", defaultValue = "NOT_FOLLOWING") + FollowStatus followStatus) { + public static FollowerDeletedResponse from(FollowStatus followStatus) { + return new FollowerDeletedResponse(followStatus); + } + ; +} 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 0d8ffad4e..f5a05bafa 100644 --- a/src/test/java/com/depromeet/domain/follow/application/FollowServiceTest.java +++ b/src/test/java/com/depromeet/domain/follow/application/FollowServiceTest.java @@ -8,10 +8,7 @@ import com.depromeet.domain.follow.domain.MemberRelation; import com.depromeet.domain.follow.dto.request.FollowCreateRequest; import com.depromeet.domain.follow.dto.request.FollowDeleteRequest; -import com.depromeet.domain.follow.dto.response.FollowFindMeInfoResponse; -import com.depromeet.domain.follow.dto.response.FollowFindTargetInfoResponse; -import com.depromeet.domain.follow.dto.response.FollowStatus; -import com.depromeet.domain.follow.dto.response.MemberFollowedResponse; +import com.depromeet.domain.follow.dto.response.*; import com.depromeet.domain.member.dao.MemberRepository; import com.depromeet.domain.member.domain.Member; import com.depromeet.domain.member.domain.Profile; @@ -496,4 +493,90 @@ class 내가_팔로우한_유저_정보_리스트를_조회할_때 { assertEquals("targetMember2", response.get(1).nickname()); } } + + @Nested + class 나의_팔로워를_삭제할_때 { + @Test + void 로그인된_회원이_존재하지_않는다면_예외를_발생시킨다() { + // when, then + assertThatThrownBy(() -> followService.deleteFollower(224L)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.MEMBER_NOT_FOUND.getMessage()); + } + + @Test + void 삭제하려는_회원이_존재하지_않는다면_예외를_발생시킨다() { + // given + Member currentMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("currentMember", "currentMember"))); + + // when, then + assertThatThrownBy(() -> followService.deleteFollower(224L)) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.FOLLOW_TARGET_MEMBER_NOT_FOUND.getMessage()); + } + + @Test + void 삭제하려는_유저가_나의_팔로워가_아니라면_예외가_발생한다() { + // given + Member currentMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("currentMember", "currentMember"))); + Member targetMember1 = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("targetMember1", "targetMember1"))); + // when, then + assertThatThrownBy(() -> followService.deleteFollower(targetMember1.getId())) + .isInstanceOf(CustomException.class) + .hasMessage(ErrorCode.FOLLOW_NOT_EXIST.getMessage()); + } + + @Test + void 내가_팔로우_하고_있다면_FOLLOWER_STATUS가_FOLLOWING로_응답한다() { + // given + Member currentMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("currentMember", "currentMember"))); + Member targetMember1 = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("targetMember1", "targetMember1"))); + memberRelationRepository.save( + MemberRelation.createMemberRelation(targetMember1, currentMember)); + memberRelationRepository.save( + MemberRelation.createMemberRelation(currentMember, targetMember1)); + + // when + FollowerDeletedResponse response = followService.deleteFollower(targetMember1.getId()); + + // then + assertEquals(FollowStatus.FOLLOWING, response.followStatus()); + } + + @Test + void 내가_팔로우_하고_있지_않다면_FOLLOWER_STATUS가_NOT_FOLLOWING로_응답한다() { + // given + Member currentMember = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("currentMember", "currentMember"))); + Member targetMember1 = + memberRepository.save( + Member.createNormalMember( + Profile.createProfile("targetMember1", "targetMember1"))); + memberRelationRepository.save( + MemberRelation.createMemberRelation(targetMember1, currentMember)); + + // when + FollowerDeletedResponse response = followService.deleteFollower(targetMember1.getId()); + + // then + assertEquals(FollowStatus.NOT_FOLLOWING, response.followStatus()); + } + } } From 549dd6a862e49663de906018d07d3b1bcdf5be71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=AA=A8?= Date: Wed, 7 Feb 2024 23:30:33 +0900 Subject: [PATCH 3/8] =?UTF-8?q?hotfix:=20=ED=91=B8=EC=8B=9C=20=EC=95=8C?= =?UTF-8?q?=EB=A6=BC=20=EC=8B=9C=20=EB=8B=89=EB=84=A4=EC=9E=84=EC=9D=B4=20?= =?UTF-8?q?username=EC=9D=B4=20=EC=95=84=EB=8B=8C=20nickname=EC=9D=84=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD=20(#290)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit hotfix: 푸시 알림 시 닉네임이 username이 아닌 nickname을 사용하도록 변경 --- .../com/depromeet/domain/follow/application/FollowService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 798346207..e14232137 100644 --- a/src/main/java/com/depromeet/domain/follow/application/FollowService.java +++ b/src/main/java/com/depromeet/domain/follow/application/FollowService.java @@ -53,7 +53,7 @@ public void createFollow(FollowCreateRequest request) { fcmService.sendMessageSync( targetMember.getFcmInfo().getFcmToken(), PUSH_SERVICE_TITLE, - String.format(PUSH_SERVICE_CONTENT, currentMember.getUsername())); + String.format(PUSH_SERVICE_CONTENT, currentMember.getProfile().getNickname())); notificationService.createNotification( NotificationType.FOLLOW, currentMember, targetMember); From 53b12042a8afdd16d5a7c9468b5d7436b1883e62 Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Thu, 8 Feb 2024 00:02:23 +0900 Subject: [PATCH 4/8] =?UTF-8?q?fix:=20=EB=AA=A8=EB=B0=94=EC=9D=BC=20?= =?UTF-8?q?=EA=B0=9C=EB=B0=9C=ED=99=98=EA=B2=BD=20=ED=85=8C=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=EB=A5=BC=20=EC=9C=84=ED=95=B4=20CORS=20=EC=A0=95?= =?UTF-8?q?=EC=B1=85=EC=97=90=20ngrok=20=EB=8F=84=EB=A9=94=EC=9D=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#292)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: CORS 설정에 ngrok URL 패턴 추가 * fix: CORS 설정에 로컬호스트 https 도메인 추가 --- .../com/depromeet/global/common/constants/UrlConstants.java | 3 +++ .../depromeet/global/config/security/WebSecurityConfig.java | 2 ++ 2 files changed, 5 insertions(+) diff --git a/src/main/java/com/depromeet/global/common/constants/UrlConstants.java b/src/main/java/com/depromeet/global/common/constants/UrlConstants.java index 501277f99..eefb6fb6d 100644 --- a/src/main/java/com/depromeet/global/common/constants/UrlConstants.java +++ b/src/main/java/com/depromeet/global/common/constants/UrlConstants.java @@ -13,6 +13,9 @@ public enum UrlConstants { PROD_DOMAIN_URL("https://www.10mm.today"), DEV_DOMAIN_URL("https://www.dev.10mm.today"), LOCAL_DOMAIN_URL("http://localhost:3000"), + LOCAL_SECURE_DOMAIN_URL("https://localhost:3000"), + + NGROK_DOMAIN_URL("https://*.ngrok-free.app"), IMAGE_DOMAIN_URL("https://image.10mm.today"), ; diff --git a/src/main/java/com/depromeet/global/config/security/WebSecurityConfig.java b/src/main/java/com/depromeet/global/config/security/WebSecurityConfig.java index b149a7118..49aa28b35 100644 --- a/src/main/java/com/depromeet/global/config/security/WebSecurityConfig.java +++ b/src/main/java/com/depromeet/global/config/security/WebSecurityConfig.java @@ -126,6 +126,8 @@ public CorsConfigurationSource corsConfigurationSource() { if (springEnvironmentUtil.isDevProfile()) { configuration.addAllowedOriginPattern(UrlConstants.DEV_DOMAIN_URL.getValue()); configuration.addAllowedOriginPattern(UrlConstants.LOCAL_DOMAIN_URL.getValue()); + configuration.addAllowedOriginPattern(UrlConstants.LOCAL_SECURE_DOMAIN_URL.getValue()); + configuration.addAllowedOriginPattern(UrlConstants.NGROK_DOMAIN_URL.getValue()); } configuration.addAllowedHeader("*"); From 56531b670c956c9ec7db491f882ac27de34bd807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EB=8F=84=EB=AA=A8?= Date: Thu, 8 Feb 2024 18:18:04 +0900 Subject: [PATCH 5/8] =?UTF-8?q?=ED=8C=94=EB=A1=9C=EC=9A=B0=20=EC=B7=A8?= =?UTF-8?q?=EC=86=8C=20=EC=8B=9C=20=EC=95=8C=EB=A6=BC=20=ED=9E=88=EC=8A=A4?= =?UTF-8?q?=ED=86=A0=EB=A6=AC=20=EC=82=AD=EC=A0=9C=20(#286)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 팔로우 취소 시 알림 히스토리 삭제 * remove: .gitkeep 파일 삭제 * fix: notification 조회 시 NotificationType도 받도록 수정 --- .../follow/application/FollowService.java | 19 +++++++++++---- .../domain/notification/api/.gitkeep | 0 .../application/NotificationService.java | 24 ------------------- .../dao/NotificationRepository.java | 7 +++++- .../domain/notification/dto/.gitkeep | 0 5 files changed, 20 insertions(+), 30 deletions(-) delete mode 100644 src/main/java/com/depromeet/domain/notification/api/.gitkeep delete mode 100644 src/main/java/com/depromeet/domain/notification/application/NotificationService.java delete mode 100644 src/main/java/com/depromeet/domain/notification/dto/.gitkeep 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 e14232137..34c488f91 100644 --- a/src/main/java/com/depromeet/domain/follow/application/FollowService.java +++ b/src/main/java/com/depromeet/domain/follow/application/FollowService.java @@ -11,7 +11,8 @@ import com.depromeet.domain.mission.domain.Mission; import com.depromeet.domain.missionRecord.domain.ImageUploadStatus; import com.depromeet.domain.missionRecord.domain.MissionRecord; -import com.depromeet.domain.notification.application.NotificationService; +import com.depromeet.domain.notification.dao.NotificationRepository; +import com.depromeet.domain.notification.domain.Notification; import com.depromeet.domain.notification.domain.NotificationType; import com.depromeet.global.config.fcm.FcmService; import com.depromeet.global.error.exception.CustomException; @@ -27,7 +28,7 @@ @RequiredArgsConstructor @Transactional public class FollowService { - private final NotificationService notificationService; + private final NotificationRepository notificationRepository; private final MemberUtil memberUtil; private final MemberRepository memberRepository; private final MemberRelationRepository memberRelationRepository; @@ -54,9 +55,10 @@ public void createFollow(FollowCreateRequest request) { targetMember.getFcmInfo().getFcmToken(), PUSH_SERVICE_TITLE, String.format(PUSH_SERVICE_CONTENT, currentMember.getProfile().getNickname())); - notificationService.createNotification( - NotificationType.FOLLOW, currentMember, targetMember); - + Notification notification = + Notification.createNotification( + NotificationType.FOLLOW, currentMember, targetMember); + notificationRepository.save(notification); memberRelationRepository.save(memberRelation); } @@ -69,6 +71,13 @@ public void deleteFollow(FollowDeleteRequest request) { .findBySourceIdAndTargetId(currentMember.getId(), targetMember.getId()) .orElseThrow(() -> new CustomException(ErrorCode.FOLLOW_NOT_EXIST)); + Optional optionalNotification = + notificationRepository.findBySourceMemberIdAndTargetMemberIdAndNotificationType( + currentMember.getId(), targetMember.getId(), NotificationType.FOLLOW); + if (optionalNotification.isPresent()) { + Notification notification = optionalNotification.get(); + notificationRepository.delete(notification); + } memberRelationRepository.delete(memberRelation); } diff --git a/src/main/java/com/depromeet/domain/notification/api/.gitkeep b/src/main/java/com/depromeet/domain/notification/api/.gitkeep deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/main/java/com/depromeet/domain/notification/application/NotificationService.java b/src/main/java/com/depromeet/domain/notification/application/NotificationService.java deleted file mode 100644 index e3bc361fd..000000000 --- a/src/main/java/com/depromeet/domain/notification/application/NotificationService.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.depromeet.domain.notification.application; - -import com.depromeet.domain.member.domain.Member; -import com.depromeet.domain.notification.dao.NotificationRepository; -import com.depromeet.domain.notification.domain.Notification; -import com.depromeet.domain.notification.domain.NotificationType; -import lombok.RequiredArgsConstructor; -import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Transactional; - -@Service -@RequiredArgsConstructor -@Transactional -public class NotificationService { - - private final NotificationRepository notificationRepository; - - public void createNotification( - NotificationType notificationType, Member currentMember, Member targetMember) { - Notification notification = - Notification.createNotification(notificationType, currentMember, targetMember); - notificationRepository.save(notification); - } -} 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 1cd6cb78b..0b70f4801 100644 --- a/src/main/java/com/depromeet/domain/notification/dao/NotificationRepository.java +++ b/src/main/java/com/depromeet/domain/notification/dao/NotificationRepository.java @@ -1,6 +1,11 @@ package com.depromeet.domain.notification.dao; import com.depromeet.domain.notification.domain.Notification; +import com.depromeet.domain.notification.domain.NotificationType; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; -public interface NotificationRepository extends JpaRepository {} +public interface NotificationRepository extends JpaRepository { + Optional findBySourceMemberIdAndTargetMemberIdAndNotificationType( + Long sourceId, Long targetId, NotificationType notificationType); +} diff --git a/src/main/java/com/depromeet/domain/notification/dto/.gitkeep b/src/main/java/com/depromeet/domain/notification/dto/.gitkeep deleted file mode 100644 index e69de29bb..000000000 From 33daf3af543ec12e02836d36f5525fc7db08d926 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Thu, 8 Feb 2024 07:22:59 -0800 Subject: [PATCH 6/8] =?UTF-8?q?fix:=20=EB=8B=89=EB=84=A4=EC=9E=84=20blank?= =?UTF-8?q?=20=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20(#294)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix: 닉네임 blank 예외 처리 * fix: 닉네임 blank 예외 처리 * fix: escapingNickname로 변경 * fix: isEmpty -> isBlank * fix: 테스트 케이스 변경 --- .../depromeet/domain/member/application/MemberService.java | 3 +++ .../domain/member/application/MemberServiceTest.java | 4 ++-- 2 files changed, 5 insertions(+), 2 deletions(-) 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 1baca346a..d94f9551c 100644 --- a/src/main/java/com/depromeet/domain/member/application/MemberService.java +++ b/src/main/java/com/depromeet/domain/member/application/MemberService.java @@ -81,6 +81,9 @@ private void validateNicknameNotDuplicate(String nickname) { public List searchMemberNickname(String nickname) { final Member currentMember = memberUtil.getCurrentMember(); final String escapingNickname = escapeSpecialCharacters(nickname); + if (escapingNickname.isBlank()) { + return List.of(); + } List members = memberRepository.nicknameSearch( diff --git a/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java b/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java index 319a4fa05..a3a12112c 100644 --- a/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java +++ b/src/test/java/com/depromeet/domain/member/application/MemberServiceTest.java @@ -200,7 +200,7 @@ void setUp() { } @Test - void 검색_키워드에_공백에_따른_처리만_허용한다() { + void 검색_키워드에_공백만_존재하는_경우_SIZE는_0이다() { // given memberRepository.save(Member.createNormalMember(Profile.createProfile("바보", "도모 얼굴"))); memberRepository.save( @@ -216,7 +216,7 @@ void setUp() { memberService.searchMemberNickname(searchNickname); // then - assertEquals(3, responses.size()); + assertEquals(0, responses.size()); } @Test From ca85e556349ffaf75582492a08d41ae48c8c6fa4 Mon Sep 17 00:00:00 2001 From: yb__char <68099546+uiurihappy@users.noreply.github.com> Date: Thu, 8 Feb 2024 07:53:18 -0800 Subject: [PATCH 7/8] =?UTF-8?q?feat:=20=EC=A2=85=EB=A3=8C=EB=AF=B8?= =?UTF-8?q?=EC=85=98=20=EB=B3=B4=EA=B4=80=ED=95=A8=20API=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84=20(#178)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: 종료미션 중간 커밋 2024.01.15 * feat: 종료미션 보관함 v1 * fix: long casting 이슈 해결 * feat: 2024.01.18 임시 커밋(스케줄러 작업 이후 진행) * fix: spotlessApply * fix: durationStatus 조건으로 변경 * fix: 종료미션 조건 수정 * fix: spotlessApply * feat: 종료미션 slice -> list * feat: 종료미션 달성률 포함 추가 및 개선 * fix: 날짜 between 계산 수정 --- .../domain/mission/api/MissionController.java | 7 +++ .../mission/application/MissionService.java | 39 ++++++++++++++++- .../mission/dao/MissionRepositoryCustom.java | 2 + .../mission/dao/MissionRepositoryImpl.java | 38 ++++++++++++++++ .../dto/response/FinishedMissionResponse.java | 43 +++++++++++++++++++ 5 files changed, 127 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/depromeet/domain/mission/dto/response/FinishedMissionResponse.java diff --git a/src/main/java/com/depromeet/domain/mission/api/MissionController.java b/src/main/java/com/depromeet/domain/mission/api/MissionController.java index ac2b0047f..caa807264 100644 --- a/src/main/java/com/depromeet/domain/mission/api/MissionController.java +++ b/src/main/java/com/depromeet/domain/mission/api/MissionController.java @@ -3,6 +3,7 @@ import com.depromeet.domain.mission.application.MissionService; import com.depromeet.domain.mission.dto.request.MissionCreateRequest; import com.depromeet.domain.mission.dto.request.MissionUpdateRequest; +import com.depromeet.domain.mission.dto.response.FinishedMissionResponse; import com.depromeet.domain.mission.dto.response.FollowMissionFindAllResponse; import com.depromeet.domain.mission.dto.response.MissionCreateResponse; import com.depromeet.domain.mission.dto.response.MissionFindAllResponse; @@ -66,6 +67,12 @@ public MissionRecordSummaryResponse missionRecordFindSummary() { return missionService.findSummaryMissionRecord(); } + @Operation(summary = "종료미션 보관함", description = "종료된 미션 리스트를 조회합니다.") + @GetMapping("/finished") + public List missionFindAllFinished() { + return missionService.findAllFinishedMission(); + } + @Operation(summary = "번개 스택 조회", description = "완료한 미션 대상으로 번개 스택을 조회합니다.") @GetMapping("/symbol/{memberId}") public MissionSymbolStackResponse missionSymbolStackFind(@PathVariable Long memberId) { 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 d24edee7c..fd0ef7f8b 100644 --- a/src/main/java/com/depromeet/domain/mission/application/MissionService.java +++ b/src/main/java/com/depromeet/domain/mission/application/MissionService.java @@ -15,6 +15,7 @@ import com.depromeet.global.error.exception.CustomException; import com.depromeet.global.error.exception.ErrorCode; import com.depromeet.global.util.MemberUtil; +import com.depromeet.global.util.SecurityUtil; import java.time.Duration; import java.time.LocalDate; import java.time.LocalDateTime; @@ -35,6 +36,7 @@ public class MissionService { private final MissionRecordTtlRepository missionRecordTtlRepository; private final MemberRelationRepository memberRelationRepository; private final MemberUtil memberUtil; + private final SecurityUtil securityUtil; public MissionCreateResponse createMission(MissionCreateRequest missionCreateRequest) { Mission mission = createMissionEntity(missionCreateRequest); @@ -116,8 +118,8 @@ public MissionRecordSummaryResponse findSummaryMissionRecord() { missions.stream() .mapToLong( mission -> - Duration.between(mission.getStartedAt().minusDays(1), today) - .toDays()) + Duration.between(mission.getStartedAt(), today).toDays() + + 1) .sum(); // Duration을 초로 바꾸고 합산 long sumDuration = @@ -201,6 +203,39 @@ public MissionSymbolStackResponse findMissionSymbolStack(Long memberId) { return MissionSymbolStackResponse.of(symbolStack); } + @Transactional(readOnly = true) + public List findAllFinishedMission() { + Long currentMemberId = securityUtil.getCurrentMemberId(); + + List finishedMissions = missionRepository.findAllFinishedMission(currentMemberId); + + return finishedMissions.stream() + .map( + mission -> { + long totalMissionDay = + Duration.between( + mission.getStartedAt(), + mission.getFinishedAt()) + .toDays() + + 1; + long completeCount = + mission.getMissionRecords().stream() + .filter( + missionRecord -> + missionRecord + .getUploadStatus() + .equals( + ImageUploadStatus + .COMPLETE)) + .count(); + + return FinishedMissionResponse.of( + mission, + calculateMissionAttainRate(completeCount, totalMissionDay)); + }) + .toList(); + } + public MissionUpdateResponse updateMission( MissionUpdateRequest missionUpdateRequest, Long missionId) { Mission mission = 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 211e83f2f..085b4e967 100644 --- a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryCustom.java +++ b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryCustom.java @@ -13,4 +13,6 @@ public interface MissionRepositoryCustom { List findMissionsWithRecordsByRelations(Long memberId, boolean existsMemberRelations); void updateFinishedDurationStatus(LocalDateTime today); + + List findAllFinishedMission(Long memberId); } 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 8eec6bf2d..6ce7a6f29 100644 --- a/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java +++ b/src/main/java/com/depromeet/domain/mission/dao/MissionRepositoryImpl.java @@ -13,6 +13,9 @@ import java.time.LocalDateTime; import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Slice; +import org.springframework.data.domain.SliceImpl; import org.springframework.stereotype.Repository; @Repository @@ -70,6 +73,18 @@ public void updateFinishedDurationStatus(LocalDateTime today) { .execute(); } + @Override + public List findAllFinishedMission(Long memberId) { + return jpaQueryFactory + .selectFrom(mission) + .leftJoin(mission.missionRecords, missionRecord) + .fetchJoin() + .where(memberIdEq(memberId), durationStatusFinishedEq()) + .orderBy(mission.finishedAt.desc()) + .fetch(); + } + + // 미션의 사용자 id 조건 검증 메서드 private BooleanExpression memberIdEq(Long memberId) { return memberId == null ? null : mission.member.id.eq(memberId); } @@ -83,4 +98,27 @@ private BooleanExpression visibilityByRelations(boolean existsRelations) { private BooleanExpression durationStatusInProgress() { return mission.durationStatus.in(DurationStatus.IN_PROGRESS); } + + // lastId보다 작은 미션 id 찾는 조건 메서드 (lastId 가 있다면 마지막 요청) + private BooleanExpression ltMissionId(Long lastId) { + return lastId == null ? null : mission.id.lt(lastId); + } + + private BooleanExpression durationStatusFinishedEq() { + return mission.durationStatus.eq(DurationStatus.FINISHED); + } + + // 무한 스크롤 방식 처리하는 메서드 + private Slice checkLastPage(int size, List result) { + + boolean hasNext = false; + + // 조회한 결과 개수가 요청한 페이지 사이즈보다 크면 뒤에 더 있음, next = true + if (result.size() > size) { + hasNext = true; + result.remove(size); + } + Pageable pageable = Pageable.unpaged(); + return new SliceImpl<>(result, pageable, hasNext); + } } diff --git a/src/main/java/com/depromeet/domain/mission/dto/response/FinishedMissionResponse.java b/src/main/java/com/depromeet/domain/mission/dto/response/FinishedMissionResponse.java new file mode 100644 index 000000000..4a4d59dea --- /dev/null +++ b/src/main/java/com/depromeet/domain/mission/dto/response/FinishedMissionResponse.java @@ -0,0 +1,43 @@ +package com.depromeet.domain.mission.dto.response; + +import com.depromeet.domain.mission.domain.Mission; +import com.depromeet.domain.mission.domain.MissionCategory; +import com.fasterxml.jackson.annotation.JsonFormat; +import io.swagger.v3.oas.annotations.media.Schema; +import java.time.LocalDateTime; + +public record FinishedMissionResponse( + @Schema(description = "미션 ID", defaultValue = "1") Long missionId, + @Schema(description = "미션 이름", defaultValue = "default name") String name, + @Schema(description = "미션 내용", defaultValue = "default content") String content, + @Schema(description = "미션 카테고리", defaultValue = "STUDY") MissionCategory category, + @Schema(description = "미션 달성률", defaultValue = "1.1") double missionAttainRate, + @JsonFormat( + shape = JsonFormat.Shape.STRING, + pattern = "yyyy-MM-dd HH:mm:ss", + timezone = "Asia/Seoul") + @Schema( + description = "미션 시작 시간", + defaultValue = "2024-01-01 00:34: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-15 00:34:00", + type = "string") + LocalDateTime finishedAt) { + public static FinishedMissionResponse of(Mission mission, double missionAttainRate) { + return new FinishedMissionResponse( + mission.getId(), + mission.getName(), + mission.getContent(), + mission.getCategory(), + missionAttainRate, + mission.getStartedAt(), + mission.getFinishedAt()); + } +} From 0e86cbbda52115fbef0a64ded333a1c7d13e4f9b Mon Sep 17 00:00:00 2001 From: Jaehyun Ahn <91878695+uwoobeat@users.noreply.github.com> Date: Fri, 9 Feb 2024 13:39:06 +0900 Subject: [PATCH 8/8] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=8B=9C=20=EB=A9=A4=EB=B2=84=20ID?= =?UTF-8?q?=EB=A5=BC=20=EC=9D=91=EB=8B=B5=20=EB=B0=94=EB=94=94=EC=97=90=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80=20(#299)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit feat: 소셜 로그인 응답에 멤버 ID 추가 --- .../depromeet/domain/auth/application/AuthService.java | 2 +- .../domain/auth/dto/response/SocialLoginResponse.java | 9 +++++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/depromeet/domain/auth/application/AuthService.java b/src/main/java/com/depromeet/domain/auth/application/AuthService.java index 6af856d49..3f20dc6cd 100644 --- a/src/main/java/com/depromeet/domain/auth/application/AuthService.java +++ b/src/main/java/com/depromeet/domain/auth/application/AuthService.java @@ -98,7 +98,7 @@ public SocialLoginResponse socialLoginMember(IdTokenRequest request, OauthProvid TokenPairResponse loginResponse = getLoginResponse(member); - return SocialLoginResponse.from(loginResponse); + return SocialLoginResponse.from(member, loginResponse); } private Member fetchOrCreate(OidcUser oidcUser) { diff --git a/src/main/java/com/depromeet/domain/auth/dto/response/SocialLoginResponse.java b/src/main/java/com/depromeet/domain/auth/dto/response/SocialLoginResponse.java index 9ef9190f7..9cca6c7db 100644 --- a/src/main/java/com/depromeet/domain/auth/dto/response/SocialLoginResponse.java +++ b/src/main/java/com/depromeet/domain/auth/dto/response/SocialLoginResponse.java @@ -1,14 +1,19 @@ package com.depromeet.domain.auth.dto.response; +import com.depromeet.domain.member.domain.Member; import io.swagger.v3.oas.annotations.media.Schema; public record SocialLoginResponse( + @Schema(description = "멤버 ID", defaultValue = "1") Long memberId, @Schema(description = "엑세스 토큰", defaultValue = "accessToken") String accessToken, @Schema(description = "리프레시 토큰", defaultValue = "refreshToken") String refreshToken, @Schema(description = "게스트 여부", defaultValue = "false") boolean isGuest) { - public static SocialLoginResponse from(TokenPairResponse tokenPairResponse) { + public static SocialLoginResponse from(Member member, TokenPairResponse tokenPairResponse) { return new SocialLoginResponse( - tokenPairResponse.accessToken(), tokenPairResponse.refreshToken(), false); + member.getId(), + tokenPairResponse.accessToken(), + tokenPairResponse.refreshToken(), + false); } }