-
Notifications
You must be signed in to change notification settings - Fork 0
Friend 도메인 API 구현 #22
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
5 commits
Select commit
Hold shift + click to select a range
cef88ab
feat: #21 :: implement friend domain api
hej090224 98a5015
fix: #21 :: address PR review feedback on friend domain
hej090224 0af5b2b
test: #21 :: update tests for review fixes and extract shared IT base
hej090224 f2df9b5
Merge remote-tracking branch 'origin/develop' into feature/21-friend-…
hej090224 a7a69a5
fix: #21 :: renumber friend migration to V5 after V4 collision with #20
hej090224 File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
20 changes: 20 additions & 0 deletions
20
src/main/kotlin/team/cklob/mudda/domain/friend/application/impl/DeleteFriendService.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,20 @@ | ||
| package team.cklob.mudda.domain.friend.application.impl | ||
|
|
||
| import org.springframework.stereotype.Service | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import team.cklob.mudda.domain.friend.domain.repository.FriendRepository | ||
| import team.cklob.mudda.domain.friend.domain.type.FriendRequestStatus | ||
| import team.cklob.mudda.global.exception.BusinessException | ||
| import team.cklob.mudda.global.exception.ErrorCode | ||
|
|
||
| @Service | ||
| class DeleteFriendService( | ||
| private val friendRepository: FriendRepository, | ||
| ) { | ||
| @Transactional | ||
| fun execute(memberId: Long, targetMemberId: Long) { | ||
| val relations = friendRepository.findByRequesterIdAndReceiverIdOrRequesterIdAndReceiverId(memberId, targetMemberId, targetMemberId, memberId) | ||
| val friend = relations.firstOrNull { it.status == FriendRequestStatus.ACCEPTED } ?: throw BusinessException(ErrorCode.FRIEND_NOT_FOUND) | ||
| friendRepository.delete(friend) | ||
| } | ||
| } |
30 changes: 30 additions & 0 deletions
30
src/main/kotlin/team/cklob/mudda/domain/friend/application/impl/GetFriendListService.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,30 @@ | ||
| package team.cklob.mudda.domain.friend.application.impl | ||
|
|
||
| import org.springframework.data.domain.Pageable | ||
| import org.springframework.stereotype.Service | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import team.cklob.mudda.domain.friend.domain.entity.Friend | ||
| import team.cklob.mudda.domain.friend.domain.repository.FriendRepository | ||
| import team.cklob.mudda.domain.friend.presentation.response.FriendPageResponse | ||
| import team.cklob.mudda.domain.friend.presentation.response.FriendResponse | ||
| import team.cklob.mudda.domain.member.domain.entity.Member | ||
|
|
||
| @Service | ||
| class GetFriendListService( | ||
| private val friendRepository: FriendRepository, | ||
| ) { | ||
| @Transactional(readOnly = true) | ||
| fun execute(memberId: Long, pageable: Pageable): FriendPageResponse<FriendResponse> { | ||
| // Blocked counterparts are already excluded by FriendRepository#findFriendships itself (NOT EXISTS | ||
| // in SQL), so the page's totalElements/totalPages/hasNext are accurate as-is -- no post-fetch | ||
| // filtering needed here. | ||
| val page = friendRepository.findFriendships(memberId, pageable) | ||
| // accepted_at is backed by ck_friend_accepted_at (see V4 migration): the DB itself guarantees an | ||
| // ACCEPTED row always has a non-null accepted_at, so this can never actually throw. | ||
| val content = page.content.map { FriendResponse.of(counterpart(it, memberId), requireNotNull(it.acceptedAt)) } | ||
|
|
||
| return FriendPageResponse.of(page, content) | ||
| } | ||
|
|
||
| private fun counterpart(friend: Friend, memberId: Long): Member = if (friend.requester.id == memberId) friend.receiver else friend.requester | ||
| } |
32 changes: 32 additions & 0 deletions
32
...ain/kotlin/team/cklob/mudda/domain/friend/application/impl/GetFriendRequestListService.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,32 @@ | ||
| package team.cklob.mudda.domain.friend.application.impl | ||
|
|
||
| import org.springframework.data.domain.Page | ||
| import org.springframework.data.domain.Pageable | ||
| import org.springframework.stereotype.Service | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import team.cklob.mudda.domain.friend.domain.entity.Friend | ||
| import team.cklob.mudda.domain.friend.domain.repository.FriendRepository | ||
| import team.cklob.mudda.domain.friend.domain.type.FriendRequestStatus | ||
| import team.cklob.mudda.domain.friend.domain.type.FriendRequestType | ||
| import team.cklob.mudda.domain.friend.presentation.response.FriendPageResponse | ||
| import team.cklob.mudda.domain.friend.presentation.response.FriendRequestResponse | ||
|
|
||
| @Service | ||
| class GetFriendRequestListService( | ||
| private val friendRepository: FriendRepository, | ||
| ) { | ||
| @Transactional(readOnly = true) | ||
| fun execute(memberId: Long, type: FriendRequestType, status: FriendRequestStatus, pageable: Pageable): FriendPageResponse<FriendRequestResponse> { | ||
| val page: Page<Friend> = when (type) { | ||
| FriendRequestType.RECEIVED -> friendRepository.findReceivedRequests(memberId, status, pageable) | ||
| FriendRequestType.SENT -> friendRepository.findSentRequests(memberId, status, pageable) | ||
| } | ||
|
|
||
| val content = page.content.map { friend -> | ||
| val counterpart = if (type == FriendRequestType.RECEIVED) friend.requester else friend.receiver | ||
| FriendRequestResponse.of(friend, type, counterpart) | ||
| } | ||
|
|
||
| return FriendPageResponse.of(page, content) | ||
| } | ||
| } |
45 changes: 45 additions & 0 deletions
45
...ain/kotlin/team/cklob/mudda/domain/friend/application/impl/RespondFriendRequestService.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,45 @@ | ||
| package team.cklob.mudda.domain.friend.application.impl | ||
|
|
||
| import org.springframework.stereotype.Service | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import team.cklob.mudda.domain.block.domain.repository.BlockRepository | ||
| import team.cklob.mudda.domain.friend.domain.repository.FriendRepository | ||
| import team.cklob.mudda.domain.friend.domain.type.FriendRequestAction | ||
| import team.cklob.mudda.domain.friend.domain.type.FriendRequestStatus | ||
| import team.cklob.mudda.domain.friend.presentation.request.RespondFriendRequestRequest | ||
| import team.cklob.mudda.global.exception.BusinessException | ||
| import team.cklob.mudda.global.exception.ErrorCode | ||
| import java.time.LocalDateTime | ||
|
|
||
| @Service | ||
| class RespondFriendRequestService( | ||
| private val friendRepository: FriendRepository, | ||
| private val blockRepository: BlockRepository, | ||
| ) { | ||
| @Transactional | ||
| fun execute(memberId: Long, requestId: Long, request: RespondFriendRequestRequest) { | ||
| val friend = friendRepository.findById(requestId).orElseThrow { BusinessException(ErrorCode.FRIEND_REQUEST_NOT_FOUND) } | ||
| if (friend.receiver.id != memberId) throw BusinessException(ErrorCode.FRIEND_REQUEST_NOT_RECEIVER) | ||
| if (friend.status != FriendRequestStatus.PENDING) throw BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_PROCESSED) | ||
|
|
||
| when (request.action) { | ||
| FriendRequestAction.ACCEPT -> { | ||
| val requesterId = requireNotNull(friend.requester.id) | ||
| // SendFriendRequestService only checks for a block at the moment the request is sent. A block | ||
| // created afterwards, while the request is still PENDING, must not be bypassed by simply | ||
| // accepting it -- re-verify here too. (Direction doesn't need to be distinguished the way | ||
| // SendFriendRequestService does: whichever side is blocked, the member calling this endpoint is | ||
| // the receiver, so a BLOCKED_MEMBER response never tells them something about the other party | ||
| // they couldn't already infer from being unable to accept.) | ||
| if (blockRepository.existsByBlockerIdAndBlockedIdOrBlockerIdAndBlockedId(memberId, requesterId, requesterId, memberId)) { | ||
| throw BusinessException(ErrorCode.BLOCKED_MEMBER) | ||
| } | ||
| friend.status = FriendRequestStatus.ACCEPTED | ||
| friend.acceptedAt = LocalDateTime.now() | ||
| } | ||
| FriendRequestAction.REJECT -> { | ||
| friend.status = FriendRequestStatus.REJECTED | ||
| } | ||
| } | ||
| } | ||
| } | ||
58 changes: 58 additions & 0 deletions
58
src/main/kotlin/team/cklob/mudda/domain/friend/application/impl/SearchFriendService.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,58 @@ | ||
| package team.cklob.mudda.domain.friend.application.impl | ||
|
|
||
| import org.springframework.data.domain.Pageable | ||
| import org.springframework.stereotype.Service | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import team.cklob.mudda.domain.friend.domain.entity.Friend | ||
| import team.cklob.mudda.domain.friend.domain.repository.FriendRepository | ||
| import team.cklob.mudda.domain.friend.domain.type.FriendRequestStatus | ||
| import team.cklob.mudda.domain.friend.domain.type.FriendRequestType | ||
| import team.cklob.mudda.domain.friend.domain.type.FriendStatus | ||
| import team.cklob.mudda.domain.friend.presentation.response.FriendPageResponse | ||
| import team.cklob.mudda.domain.friend.presentation.response.FriendSearchResponse | ||
| import team.cklob.mudda.domain.member.domain.repository.MemberRepository | ||
| import team.cklob.mudda.global.exception.BusinessException | ||
| import team.cklob.mudda.global.exception.ErrorCode | ||
|
|
||
| @Service | ||
| class SearchFriendService( | ||
| private val memberRepository: MemberRepository, | ||
| private val friendRepository: FriendRepository, | ||
| ) { | ||
| @Transactional(readOnly = true) | ||
| fun execute(memberId: Long, keyword: String, pageable: Pageable): FriendPageResponse<FriendSearchResponse> { | ||
| val trimmed = keyword.trim() | ||
| if (trimmed.isBlank()) throw BusinessException(ErrorCode.INVALID_SEARCH_KEYWORD) | ||
|
|
||
| val page = memberRepository.searchSelectableByNickname(memberId, trimmed, pageable) | ||
| val candidateIds = page.content.mapNotNull { it.id } | ||
| val relationsByOtherId = if (candidateIds.isEmpty()) emptyMap() else groupRelationsByOtherId(memberId, friendRepository.findAllBetween(memberId, candidateIds)) | ||
|
|
||
| val content = page.content.map { candidate -> | ||
| val relation = relationsByOtherId[candidate.id] | ||
| val (status, direction) = resolveRelation(memberId, relation) | ||
| FriendSearchResponse.of(candidate, status, relation?.id, direction) | ||
| } | ||
|
|
||
| return FriendPageResponse.of(page, content) | ||
| } | ||
|
|
||
| // A requester/receiver pair can have relationship rows in both directions (see FriendRepository), so an | ||
| // ACCEPTED row always wins over a stray PENDING row for the same pair, mirroring GetMemberProfileService. | ||
| private fun groupRelationsByOtherId(memberId: Long, relations: List<Friend>): Map<Long, Friend> = | ||
| relations.groupBy { if (it.requester.id == memberId) it.receiver.id else it.requester.id } | ||
| .mapNotNull { (otherId, rels) -> | ||
| val chosen = rels.firstOrNull { it.status == FriendRequestStatus.ACCEPTED } ?: rels.firstOrNull { it.status == FriendRequestStatus.PENDING } ?: rels.first() | ||
| otherId?.let { it to chosen } | ||
| }.toMap() | ||
|
|
||
| private fun resolveRelation(memberId: Long, relation: Friend?): Pair<FriendStatus, FriendRequestType?> { | ||
| if (relation == null) return FriendStatus.NONE to null | ||
| val sentByMe = relation.requester.id == memberId | ||
| return when (relation.status) { | ||
| FriendRequestStatus.ACCEPTED -> FriendStatus.FRIEND to (if (sentByMe) FriendRequestType.SENT else FriendRequestType.RECEIVED) | ||
| FriendRequestStatus.PENDING -> (if (sentByMe) FriendStatus.REQUESTED else FriendStatus.RECEIVED) to (if (sentByMe) FriendRequestType.SENT else FriendRequestType.RECEIVED) | ||
| FriendRequestStatus.REJECTED -> FriendStatus.NONE to null | ||
| } | ||
| } | ||
| } |
75 changes: 75 additions & 0 deletions
75
src/main/kotlin/team/cklob/mudda/domain/friend/application/impl/SendFriendRequestService.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,75 @@ | ||
| package team.cklob.mudda.domain.friend.application.impl | ||
|
|
||
| import org.slf4j.LoggerFactory | ||
| import org.springframework.dao.DataIntegrityViolationException | ||
| import org.springframework.stereotype.Service | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import team.cklob.mudda.domain.block.domain.repository.BlockRepository | ||
| import team.cklob.mudda.domain.friend.domain.entity.Friend | ||
| import team.cklob.mudda.domain.friend.domain.repository.FriendRepository | ||
| import team.cklob.mudda.domain.friend.domain.type.FriendRequestStatus | ||
| import team.cklob.mudda.domain.friend.presentation.request.SendFriendRequestRequest | ||
| import team.cklob.mudda.domain.friend.presentation.response.SendFriendRequestResponse | ||
| import team.cklob.mudda.domain.member.domain.repository.MemberRepository | ||
| import team.cklob.mudda.global.exception.AuthException | ||
| import team.cklob.mudda.global.exception.BusinessException | ||
| import team.cklob.mudda.global.exception.ErrorCode | ||
|
|
||
| @Service | ||
| class SendFriendRequestService( | ||
| private val friendRepository: FriendRepository, | ||
| private val memberRepository: MemberRepository, | ||
| private val blockRepository: BlockRepository, | ||
| ) { | ||
| private val logger = LoggerFactory.getLogger(javaClass) | ||
|
|
||
| @Transactional | ||
| fun execute(memberId: Long, request: SendFriendRequestRequest): SendFriendRequestResponse { | ||
| // @field:NotNull on SendFriendRequestRequest.receiverId already rejects a null/missing value with a | ||
| // 400 before this service runs; requireNotNull here just documents that invariant for callers. | ||
| val receiverId = requireNotNull(request.receiverId) | ||
| if (memberId == receiverId) throw BusinessException(ErrorCode.CANNOT_REQUEST_SELF) | ||
|
|
||
| val requester = memberRepository.findById(memberId).orElseThrow { AuthException(ErrorCode.UNAUTHORIZED) } | ||
| if (requester.withdrawnAt != null) throw BusinessException(ErrorCode.WITHDRAWN_MEMBER) | ||
|
|
||
| val receiver = memberRepository.findById(receiverId).orElseThrow { BusinessException(ErrorCode.MEMBER_NOT_FOUND) } | ||
| if (receiver.withdrawnAt != null || receiver.nickname == null) throw BusinessException(ErrorCode.MEMBER_NOT_FOUND) | ||
|
|
||
| // Direction matters here: if I blocked them, telling them BLOCKED_MEMBER doesn't leak anything they | ||
| // don't already know. If they blocked me, BLOCKED_MEMBER would leak the fact that a block exists | ||
| // (unlike the search API, which silently excludes blocked members via a NOT EXISTS filter) -- so | ||
| // that direction is reported as MEMBER_NOT_FOUND instead, indistinguishable from a nonexistent id. | ||
| if (blockRepository.existsByBlockerIdAndBlockedId(memberId, receiverId)) { | ||
| throw BusinessException(ErrorCode.BLOCKED_MEMBER) | ||
| } | ||
| if (blockRepository.existsByBlockerIdAndBlockedId(receiverId, memberId)) { | ||
| throw BusinessException(ErrorCode.MEMBER_NOT_FOUND) | ||
| } | ||
|
|
||
| val existingRelations = friendRepository.findByRequesterIdAndReceiverIdOrRequesterIdAndReceiverId(memberId, receiverId, receiverId, memberId) | ||
| existingRelations.forEach { relation -> | ||
| when { | ||
| relation.status == FriendRequestStatus.ACCEPTED -> throw BusinessException(ErrorCode.ALREADY_FRIENDS) | ||
| relation.status == FriendRequestStatus.PENDING && relation.requester.id == memberId -> throw BusinessException(ErrorCode.FRIEND_REQUEST_ALREADY_EXISTS) | ||
| relation.status == FriendRequestStatus.PENDING -> throw BusinessException(ErrorCode.REVERSE_FRIEND_REQUEST_EXISTS) | ||
| // A REJECTED row doesn't block a new request -- uq_friend_requester_receiver (see V4) is a | ||
| // partial index that excludes REJECTED rows, so a fresh row for the same direction can be | ||
| // inserted below even while the old REJECTED row is kept around as history. | ||
| } | ||
| } | ||
|
|
||
| val saved = try { | ||
| friendRepository.saveAndFlush(Friend(requester = requester, receiver = receiver, status = FriendRequestStatus.PENDING)) | ||
|
hej090224 marked this conversation as resolved.
|
||
| } catch (e: DataIntegrityViolationException) { | ||
| // Safety net for a concurrent insert that raced past the checks above -- most likely the reverse- | ||
| // direction pending race guarded by uq_friend_pending_pair, but could in principle be any | ||
| // constraint on this table (e.g. a member row deleted mid-request). Logged with the original | ||
| // exception since folding every violation into one error code would otherwise hide the real cause. | ||
| logger.warn("friend request insert violated a constraint: requester={}, receiver={}", memberId, receiverId, e) | ||
| throw BusinessException(ErrorCode.REVERSE_FRIEND_REQUEST_EXISTS) | ||
| } | ||
|
hej090224 marked this conversation as resolved.
|
||
|
|
||
| return SendFriendRequestResponse.from(saved) | ||
| } | ||
| } | ||
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
6 changes: 6 additions & 0 deletions
6
src/main/kotlin/team/cklob/mudda/domain/friend/domain/type/FriendRequestAction.kt
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,6 @@ | ||
| package team.cklob.mudda.domain.friend.domain.type | ||
|
|
||
| enum class FriendRequestAction { | ||
| ACCEPT, | ||
| REJECT, | ||
| } |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.