From 3314b3267c3b24f2f8382358f813992fc53b2a15 Mon Sep 17 00:00:00 2001 From: Jihoi-Kang Date: Sun, 22 Oct 2023 20:00:35 +0900 Subject: [PATCH] [FEAT] add follow and unfollow API --- .../rest/api/member/MemberApiController.kt | 47 +++++++++++++++++++ .../healthiee/rest/domain/base/BaseEntity.kt | 7 ++- .../healthiee/rest/domain/follow/Follow.kt | 4 ++ .../rest/lib/error/ApplicationErrorCode.kt | 9 +++- .../follow/FollowCustomRepository.kt | 10 ++++ .../follow/FollowCustomRepositoryImpl.kt | 23 +++++++++ .../repository/follow/FollowRepository.kt | 2 +- .../healthiee/rest/service/FollowService.kt | 38 +++++++++++++++ .../healthiee/rest/service/MemberService.kt | 14 +++--- 9 files changed, 144 insertions(+), 10 deletions(-) create mode 100644 src/main/kotlin/healthiee/rest/repository/follow/FollowCustomRepository.kt create mode 100644 src/main/kotlin/healthiee/rest/repository/follow/FollowCustomRepositoryImpl.kt create mode 100644 src/main/kotlin/healthiee/rest/service/FollowService.kt diff --git a/src/main/kotlin/healthiee/rest/api/member/MemberApiController.kt b/src/main/kotlin/healthiee/rest/api/member/MemberApiController.kt index 72ce8fa..18589e9 100644 --- a/src/main/kotlin/healthiee/rest/api/member/MemberApiController.kt +++ b/src/main/kotlin/healthiee/rest/api/member/MemberApiController.kt @@ -2,20 +2,29 @@ package healthiee.rest.api.member import healthiee.rest.api.member.dto.MemberDto import healthiee.rest.api.member.dto.response.CheckMemberResponse +import healthiee.rest.domain.member.Member +import healthiee.rest.lib.error.ApiException +import healthiee.rest.lib.error.ApplicationErrorCode.NOT_FOUND_MEMBER import healthiee.rest.lib.response.BaseResponse import healthiee.rest.repository.member.MemberRepository +import healthiee.rest.service.FollowService import healthiee.rest.service.MemberService +import org.springframework.data.repository.findByIdOrNull import org.springframework.http.HttpStatus import org.springframework.http.ResponseEntity import org.springframework.security.access.prepost.PreAuthorize +import org.springframework.security.core.annotation.AuthenticationPrincipal +import org.springframework.web.bind.annotation.DeleteMapping import org.springframework.web.bind.annotation.GetMapping import org.springframework.web.bind.annotation.PathVariable +import org.springframework.web.bind.annotation.PostMapping import org.springframework.web.bind.annotation.RestController import java.util.* @RestController class MemberApiController( private val memberService: MemberService, + private val followService: FollowService, private val memberRepository: MemberRepository, ) { @@ -34,4 +43,42 @@ class MemberApiController( return ResponseEntity.ok(BaseResponse(code = HttpStatus.OK.value(), data = CheckMemberResponse(exist))) } + @PostMapping("/v1/members/{memberId}/follow") + @PreAuthorize("hasRole('MEMBER')") + fun followMember( + @AuthenticationPrincipal member: Member, + @PathVariable("memberId") targetMemberId: UUID, + ): ResponseEntity> { + val findMember = memberRepository.findByIdOrNull(targetMemberId) + ?: throw ApiException(NOT_FOUND_MEMBER) + + followService.follow(member, findMember) + + return ResponseEntity.ok( + BaseResponse( + code = HttpStatus.OK.value(), + message = "팔로우가 완료 되었습니다", + ) + ) + } + + @DeleteMapping("/v1/members/{memberId}/follow") + @PreAuthorize("hasRole('MEMBER')") + fun unfollowMember( + @AuthenticationPrincipal member: Member, + @PathVariable("memberId") targetMemberId: UUID, + ): ResponseEntity> { + val findMember = memberRepository.findByIdOrNull(targetMemberId) + ?: throw ApiException(NOT_FOUND_MEMBER) + + followService.unfollow(member, findMember) + + return ResponseEntity.ok( + BaseResponse( + code = HttpStatus.OK.value(), + message = "팔로우가 취소 되었습니다", + ) + ) + } + } \ No newline at end of file diff --git a/src/main/kotlin/healthiee/rest/domain/base/BaseEntity.kt b/src/main/kotlin/healthiee/rest/domain/base/BaseEntity.kt index 5b3dd05..bb9366e 100644 --- a/src/main/kotlin/healthiee/rest/domain/base/BaseEntity.kt +++ b/src/main/kotlin/healthiee/rest/domain/base/BaseEntity.kt @@ -22,6 +22,11 @@ abstract class BaseEntity { protected set @Column(nullable = false) - val deleted: Boolean = false + var deleted: Boolean = false + private set + + fun delete() { + deleted = true + } } \ No newline at end of file diff --git a/src/main/kotlin/healthiee/rest/domain/follow/Follow.kt b/src/main/kotlin/healthiee/rest/domain/follow/Follow.kt index cca3f46..82bfa27 100644 --- a/src/main/kotlin/healthiee/rest/domain/follow/Follow.kt +++ b/src/main/kotlin/healthiee/rest/domain/follow/Follow.kt @@ -30,4 +30,8 @@ class Follow( @Column(name = "follow_id") val id: Long = 0L + companion object { + fun createFollow(member: Member, targetMember: Member) = Follow(member, targetMember) + } + } \ No newline at end of file diff --git a/src/main/kotlin/healthiee/rest/lib/error/ApplicationErrorCode.kt b/src/main/kotlin/healthiee/rest/lib/error/ApplicationErrorCode.kt index c58302f..7fa9837 100644 --- a/src/main/kotlin/healthiee/rest/lib/error/ApplicationErrorCode.kt +++ b/src/main/kotlin/healthiee/rest/lib/error/ApplicationErrorCode.kt @@ -5,12 +5,17 @@ import org.springframework.http.HttpStatus enum class ApplicationErrorCode( private val _httpStatus: HttpStatus, private val _message: String, -): ErrorCode { +) : ErrorCode { // Not Found NOT_FOUND_MEMBER(HttpStatus.NOT_FOUND, "멤버를 찾을 수 없습니다"), NOT_FOUND_CODE(HttpStatus.NOT_FOUND, "코드를 찾을 수 없습니다"), + NOT_FOUND_FOLLOW(HttpStatus.NOT_FOUND, "팔로우를 찾을 수 없습니다"), + // Forbidden - FORBIDDEN_INVALID_REFRESH_TOKEN(HttpStatus.FORBIDDEN, "유효하지 않는 리프레쉬 토큰입니다") + FORBIDDEN_INVALID_REFRESH_TOKEN(HttpStatus.FORBIDDEN, "유효하지 않는 리프레쉬 토큰입니다"), + + // Bad Request + BAD_REQUEST_ALREADY_EXIST_FOLLOW(HttpStatus.BAD_REQUEST, "이미 팔로우가 되어 있습니다"), ; override val httpStatus: HttpStatus diff --git a/src/main/kotlin/healthiee/rest/repository/follow/FollowCustomRepository.kt b/src/main/kotlin/healthiee/rest/repository/follow/FollowCustomRepository.kt new file mode 100644 index 0000000..a4a7f78 --- /dev/null +++ b/src/main/kotlin/healthiee/rest/repository/follow/FollowCustomRepository.kt @@ -0,0 +1,10 @@ +package healthiee.rest.repository.follow + +import healthiee.rest.domain.follow.Follow +import healthiee.rest.domain.member.Member + +interface FollowCustomRepository { + + fun findByMember(member: Member, targetMember: Member): Follow? + +} \ No newline at end of file diff --git a/src/main/kotlin/healthiee/rest/repository/follow/FollowCustomRepositoryImpl.kt b/src/main/kotlin/healthiee/rest/repository/follow/FollowCustomRepositoryImpl.kt new file mode 100644 index 0000000..ad69cc2 --- /dev/null +++ b/src/main/kotlin/healthiee/rest/repository/follow/FollowCustomRepositoryImpl.kt @@ -0,0 +1,23 @@ +package healthiee.rest.repository.follow + +import com.querydsl.jpa.impl.JPAQueryFactory +import healthiee.rest.domain.follow.Follow +import healthiee.rest.domain.follow.QFollow.follow +import healthiee.rest.domain.member.Member +import java.util.* + +class FollowCustomRepositoryImpl( + private val queryFactory: JPAQueryFactory, +) : FollowCustomRepository { + + override fun findByMember(member: Member, targetMember: Member): Follow? { + return queryFactory.selectFrom(follow) + .where( + follow.member.eq(member), + follow.targetMember.eq(targetMember), + follow.deleted.eq(false) + ) + .fetchOne() + + } +} \ No newline at end of file diff --git a/src/main/kotlin/healthiee/rest/repository/follow/FollowRepository.kt b/src/main/kotlin/healthiee/rest/repository/follow/FollowRepository.kt index 0d1b06c..880abec 100644 --- a/src/main/kotlin/healthiee/rest/repository/follow/FollowRepository.kt +++ b/src/main/kotlin/healthiee/rest/repository/follow/FollowRepository.kt @@ -6,7 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @Repository -interface FollowRepository : JpaRepository { +interface FollowRepository : JpaRepository, FollowCustomRepository { fun countByMember(member: Member): Int // 팔로우 수 fun countByTargetMember(member: Member): Int // 팔로워 수 } \ No newline at end of file diff --git a/src/main/kotlin/healthiee/rest/service/FollowService.kt b/src/main/kotlin/healthiee/rest/service/FollowService.kt new file mode 100644 index 0000000..4425748 --- /dev/null +++ b/src/main/kotlin/healthiee/rest/service/FollowService.kt @@ -0,0 +1,38 @@ +package healthiee.rest.service + +import healthiee.rest.domain.follow.Follow +import healthiee.rest.domain.member.Member +import healthiee.rest.lib.error.ApiException +import healthiee.rest.lib.error.ApplicationErrorCode.BAD_REQUEST_ALREADY_EXIST_FOLLOW +import healthiee.rest.lib.error.ApplicationErrorCode.NOT_FOUND_FOLLOW +import healthiee.rest.repository.follow.FollowRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class FollowService( + val followRepository: FollowRepository, +) { + + @Transactional + fun follow(member: Member, targetMember: Member) { + val findFollow = followRepository.findByMember(member, targetMember) + findFollow?.let { throw ApiException(BAD_REQUEST_ALREADY_EXIST_FOLLOW) } + + followRepository.save(Follow.createFollow(member, targetMember)) + } + + @Transactional + fun unfollow(member: Member, targetMember: Member) { + val findFollow = followRepository.findByMember(member, targetMember) + ?: throw ApiException(NOT_FOUND_FOLLOW) + + findFollow.delete() + } + + fun getFollowingCount(member: Member): Int = followRepository.countByMember(member) + + fun getFollowerCount(member: Member): Int = followRepository.countByTargetMember(member) + +} \ No newline at end of file diff --git a/src/main/kotlin/healthiee/rest/service/MemberService.kt b/src/main/kotlin/healthiee/rest/service/MemberService.kt index a211f23..af6b3b4 100644 --- a/src/main/kotlin/healthiee/rest/service/MemberService.kt +++ b/src/main/kotlin/healthiee/rest/service/MemberService.kt @@ -3,7 +3,6 @@ package healthiee.rest.service import healthiee.rest.api.member.dto.MemberDto import healthiee.rest.lib.error.ApiException import healthiee.rest.lib.error.ApplicationErrorCode.NOT_FOUND_MEMBER -import healthiee.rest.repository.follow.FollowRepository import healthiee.rest.repository.member.MemberRepository import org.springframework.data.repository.findByIdOrNull import org.springframework.stereotype.Service @@ -14,17 +13,20 @@ import java.util.* @Transactional(readOnly = true) class MemberService( val memberRepository: MemberRepository, - val followRepository: FollowRepository, + val followService: FollowService, ) { fun getMember(id: UUID): MemberDto { val findMember = memberRepository.findByIdOrNull(id) ?: throw ApiException(NOT_FOUND_MEMBER) - val followingCount = followRepository.countByMember(findMember) - val followerCount = followRepository.countByTargetMember(findMember) - - return MemberDto.create(MemberDto.Params(findMember, followingCount, followerCount)) + return MemberDto.create( + MemberDto.Params( + findMember, + followService.getFollowingCount(findMember), + followService.getFollowerCount(findMember), + ) + ) } } \ No newline at end of file