-
Notifications
You must be signed in to change notification settings - Fork 0
회원 프로필 조회 및 수정 API 구현 #18
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 5 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
87e7c89
refactor: #17 :: use ProfileVisibility enum instead of a string for m…
exijn 2986d41
feat: #17 :: add member not-found and profile access error codes
exijn cbeb191
feat: #17 :: add bidirectional friend relation lookup to friend repos…
exijn ee90d26
feat: #17 :: implement member info query and update apis
exijn f0931f3
test: #17 :: add tests for member profile apis
exijn 3258092
fix: #17 :: return a list from the bidirectional friend lookup to avo…
exijn 466387d
fix: #17 :: block PATCH /member/me for members that have not complete…
exijn e876093
test: #17 :: assert auth fields stay out of member responses and PATC…
exijn 43fc3b3
refactor: #17 :: make ProfileAccessPolicy's visibility check an exhau…
exijn 6fd5c4b
fix: #17 :: trim and blank-normalize member update fields, narrow the…
exijn c099316
fix: #17 :: align /member/me error codes with the auth domain's inval…
exijn c129022
fix: #17 :: drop real name from the member profile response
exijn f0b65e2
fix: #17 :: rename member API base path to /api/v1/members
exijn 5e632c3
fix: #17 :: return 400 instead of 500 for a path variable that fails …
exijn cb99237
fix: #17 :: reject non-http(s) profileImageUrl values at the request …
exijn 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
13 changes: 13 additions & 0 deletions
13
src/main/kotlin/team/cklob/mudda/domain/member/application/ProfileAccessPolicy.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,13 @@ | ||
| package team.cklob.mudda.domain.member.application | ||
|
|
||
| import team.cklob.mudda.domain.friend.domain.type.FriendStatus | ||
| import team.cklob.mudda.domain.member.domain.type.ProfileVisibility | ||
|
|
||
| object ProfileAccessPolicy { | ||
| fun canView(visibility: ProfileVisibility, isSelf: Boolean, friendStatus: FriendStatus): Boolean = when { | ||
| isSelf -> true | ||
| visibility == ProfileVisibility.PUBLIC -> true | ||
| visibility == ProfileVisibility.FRIEND -> friendStatus == FriendStatus.FRIEND | ||
| else -> false | ||
| } | ||
|
cfcromn marked this conversation as resolved.
Outdated
|
||
| } | ||
45 changes: 45 additions & 0 deletions
45
src/main/kotlin/team/cklob/mudda/domain/member/application/impl/GetMemberProfileService.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.member.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.domain.friend.domain.type.FriendStatus | ||
| import team.cklob.mudda.domain.member.application.ProfileAccessPolicy | ||
| import team.cklob.mudda.domain.member.domain.repository.MemberRepository | ||
| import team.cklob.mudda.domain.member.presentation.response.MemberProfileResponse | ||
| import team.cklob.mudda.global.exception.BusinessException | ||
| import team.cklob.mudda.global.exception.ErrorCode | ||
|
|
||
| @Service | ||
| class GetMemberProfileService( | ||
| private val memberRepository: MemberRepository, | ||
| private val friendRepository: FriendRepository, | ||
| ) { | ||
| @Transactional(readOnly = true) | ||
| fun execute(viewerId: Long, memberId: Long): MemberProfileResponse { | ||
| val member = memberRepository.findById(memberId).orElseThrow { BusinessException(ErrorCode.MEMBER_NOT_FOUND) } | ||
| if (member.withdrawnAt != null || member.nickname == null) throw BusinessException(ErrorCode.MEMBER_NOT_FOUND) | ||
|
|
||
| val isSelf = viewerId == memberId | ||
| val friendStatus = if (isSelf) FriendStatus.NONE else resolveFriendStatus(viewerId, memberId) | ||
|
|
||
| if (!ProfileAccessPolicy.canView(member.profileVisibility, isSelf, friendStatus)) { | ||
| throw BusinessException(ErrorCode.PROFILE_ACCESS_DENIED) | ||
| } | ||
|
cfcromn marked this conversation as resolved.
|
||
|
|
||
| return MemberProfileResponse.of(member, friendStatus) | ||
| } | ||
|
|
||
| private fun resolveFriendStatus(viewerId: Long, memberId: Long): FriendStatus { | ||
| val friend = friendRepository | ||
| .findByRequesterIdAndReceiverIdOrRequesterIdAndReceiverId(viewerId, memberId, memberId, viewerId) | ||
| .orElse(null) ?: return FriendStatus.NONE | ||
|
cfcromn marked this conversation as resolved.
Outdated
|
||
|
|
||
| return when (friend.status) { | ||
| FriendRequestStatus.ACCEPTED -> FriendStatus.FRIEND | ||
| FriendRequestStatus.REJECTED -> FriendStatus.NONE | ||
| FriendRequestStatus.PENDING -> if (friend.requester.id == viewerId) FriendStatus.REQUESTED else FriendStatus.RECEIVED | ||
| } | ||
| } | ||
| } | ||
20 changes: 20 additions & 0 deletions
20
src/main/kotlin/team/cklob/mudda/domain/member/application/impl/GetMyMemberService.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.member.application.impl | ||
|
|
||
| import org.springframework.stereotype.Service | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import team.cklob.mudda.domain.member.domain.repository.MemberRepository | ||
| import team.cklob.mudda.domain.member.presentation.response.MyMemberResponse | ||
| import team.cklob.mudda.global.exception.BusinessException | ||
| import team.cklob.mudda.global.exception.ErrorCode | ||
|
|
||
| @Service | ||
| class GetMyMemberService( | ||
| private val memberRepository: MemberRepository, | ||
| ) { | ||
| @Transactional(readOnly = true) | ||
| fun execute(memberId: Long): MyMemberResponse { | ||
| val member = memberRepository.findById(memberId).orElseThrow { BusinessException(ErrorCode.MEMBER_NOT_FOUND) } | ||
| if (member.withdrawnAt != null) throw BusinessException(ErrorCode.MEMBER_NOT_FOUND) | ||
|
cfcromn marked this conversation as resolved.
Outdated
|
||
| return MyMemberResponse.from(member) | ||
| } | ||
| } | ||
48 changes: 48 additions & 0 deletions
48
src/main/kotlin/team/cklob/mudda/domain/member/application/impl/UpdateMyMemberService.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,48 @@ | ||
| package team.cklob.mudda.domain.member.application.impl | ||
|
|
||
| import org.springframework.dao.DataIntegrityViolationException | ||
| import org.springframework.stereotype.Service | ||
| import org.springframework.transaction.annotation.Transactional | ||
| import team.cklob.mudda.domain.member.domain.repository.MemberRepository | ||
| import team.cklob.mudda.domain.member.presentation.request.UpdateMyMemberRequest | ||
| import team.cklob.mudda.domain.member.presentation.response.MyMemberResponse | ||
| import team.cklob.mudda.global.exception.BusinessException | ||
| import team.cklob.mudda.global.exception.ErrorCode | ||
|
|
||
| @Service | ||
| class UpdateMyMemberService( | ||
| private val memberRepository: MemberRepository, | ||
| ) { | ||
| @Transactional | ||
| fun execute(memberId: Long, request: UpdateMyMemberRequest): MyMemberResponse { | ||
| if (request.isEmpty()) throw BusinessException(ErrorCode.INVALID_INPUT) | ||
|
|
||
| val member = memberRepository.findById(memberId).orElseThrow { BusinessException(ErrorCode.MEMBER_NOT_FOUND) } | ||
| if (member.withdrawnAt != null) throw BusinessException(ErrorCode.MEMBER_NOT_FOUND) | ||
|
cfcromn marked this conversation as resolved.
Outdated
|
||
|
|
||
| request.name?.let { | ||
| if (it.isBlank()) throw BusinessException(ErrorCode.INVALID_INPUT) | ||
| member.name = it | ||
| } | ||
| request.nickname?.let { nickname -> | ||
| if (nickname.isBlank()) throw BusinessException(ErrorCode.INVALID_INPUT) | ||
| if (nickname != member.nickname && memberRepository.existsByNickname(nickname)) { | ||
| throw BusinessException(ErrorCode.NICKNAME_ALREADY_EXISTS) | ||
| } | ||
| member.nickname = nickname | ||
| } | ||
| request.gender?.let { member.gender = it } | ||
| request.birthYear?.let { member.birthYear = it } | ||
| request.profileImageUrl?.let { member.profileImageUrl = it.ifEmpty { null } } | ||
| request.bio?.let { member.bio = it.ifEmpty { null } } | ||
|
cfcromn marked this conversation as resolved.
Outdated
|
||
| request.profileVisibility?.let { member.profileVisibility = it } | ||
|
|
||
| val saved = try { | ||
| memberRepository.saveAndFlush(member) | ||
| } catch (e: DataIntegrityViolationException) { | ||
| throw BusinessException(ErrorCode.NICKNAME_ALREADY_EXISTS) | ||
| } | ||
|
cfcromn marked this conversation as resolved.
|
||
|
|
||
| return MyMemberResponse.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
7 changes: 7 additions & 0 deletions
7
src/main/kotlin/team/cklob/mudda/domain/member/domain/type/ProfileVisibility.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,7 @@ | ||
| package team.cklob.mudda.domain.member.domain.type | ||
|
|
||
| enum class ProfileVisibility { | ||
| PUBLIC, | ||
| FRIEND, | ||
| PRIVATE, | ||
| } |
42 changes: 42 additions & 0 deletions
42
src/main/kotlin/team/cklob/mudda/domain/member/presentation/controller/MemberController.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,42 @@ | ||
| package team.cklob.mudda.domain.member.presentation.controller | ||
|
|
||
| import jakarta.validation.Valid | ||
| import org.springframework.http.ResponseEntity | ||
| import org.springframework.web.bind.annotation.GetMapping | ||
| import org.springframework.web.bind.annotation.PatchMapping | ||
| import org.springframework.web.bind.annotation.PathVariable | ||
| import org.springframework.web.bind.annotation.RequestBody | ||
| import org.springframework.web.bind.annotation.RequestMapping | ||
| import org.springframework.web.bind.annotation.RestController | ||
| import team.cklob.mudda.domain.member.application.impl.GetMemberProfileService | ||
| import team.cklob.mudda.domain.member.application.impl.GetMyMemberService | ||
| import team.cklob.mudda.domain.member.application.impl.UpdateMyMemberService | ||
| import team.cklob.mudda.domain.member.presentation.request.UpdateMyMemberRequest | ||
| import team.cklob.mudda.domain.member.presentation.response.MemberProfileResponse | ||
| import team.cklob.mudda.domain.member.presentation.response.MyMemberResponse | ||
| import team.cklob.mudda.global.response.ApiResponse | ||
| import team.cklob.mudda.global.security.LoginUser | ||
|
|
||
| @RestController | ||
| @RequestMapping("/api/v1/member") | ||
| class MemberController( | ||
| private val getMyMemberService: GetMyMemberService, | ||
| private val updateMyMemberService: UpdateMyMemberService, | ||
| private val getMemberProfileService: GetMemberProfileService, | ||
| ) { | ||
| @GetMapping("/me") | ||
| fun getMe(@LoginUser memberId: Long): ResponseEntity<ApiResponse<MyMemberResponse>> = | ||
| ResponseEntity.ok(ApiResponse.success(getMyMemberService.execute(memberId))) | ||
|
|
||
| @PatchMapping("/me") | ||
| fun updateMe( | ||
| @LoginUser memberId: Long, | ||
| @Valid @RequestBody request: UpdateMyMemberRequest, | ||
| ): ResponseEntity<ApiResponse<MyMemberResponse>> = ResponseEntity.ok(ApiResponse.success(updateMyMemberService.execute(memberId, request))) | ||
|
|
||
| @GetMapping("/{memberId}") | ||
| fun getProfile( | ||
| @LoginUser viewerId: Long, | ||
| @PathVariable memberId: Long, | ||
|
cfcromn marked this conversation as resolved.
|
||
| ): ResponseEntity<ApiResponse<MemberProfileResponse>> = ResponseEntity.ok(ApiResponse.success(getMemberProfileService.execute(viewerId, memberId))) | ||
| } | ||
33 changes: 33 additions & 0 deletions
33
src/main/kotlin/team/cklob/mudda/domain/member/presentation/request/UpdateMyMemberRequest.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,33 @@ | ||
| package team.cklob.mudda.domain.member.presentation.request | ||
|
|
||
| import jakarta.validation.constraints.Max | ||
| import jakarta.validation.constraints.Min | ||
| import jakarta.validation.constraints.Size | ||
| import team.cklob.mudda.domain.member.domain.type.Gender | ||
| import team.cklob.mudda.domain.member.domain.type.ProfileVisibility | ||
|
|
||
| data class UpdateMyMemberRequest( | ||
| @field:Size(max = 30) | ||
| val name: String? = null, | ||
|
|
||
| @field:Size(max = 30) | ||
| val nickname: String? = null, | ||
|
|
||
| val gender: Gender? = null, | ||
|
|
||
| @field:Min(1900) | ||
| @field:Max(2100) | ||
| val birthYear: Int? = null, | ||
|
|
||
| @field:Size(max = 255) | ||
| val profileImageUrl: String? = null, | ||
|
cfcromn marked this conversation as resolved.
|
||
|
|
||
| @field:Size(max = 100) | ||
| val bio: String? = null, | ||
|
|
||
| val profileVisibility: ProfileVisibility? = null, | ||
| ) { | ||
| fun isEmpty(): Boolean = | ||
| name == null && nickname == null && gender == null && birthYear == null && | ||
| profileImageUrl == null && bio == null && profileVisibility == null | ||
|
cfcromn marked this conversation as resolved.
|
||
| } | ||
32 changes: 32 additions & 0 deletions
32
...main/kotlin/team/cklob/mudda/domain/member/presentation/response/MemberProfileResponse.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.member.presentation.response | ||
|
|
||
| import team.cklob.mudda.domain.friend.domain.type.FriendStatus | ||
| import team.cklob.mudda.domain.member.domain.entity.Member | ||
| import team.cklob.mudda.domain.member.domain.type.Gender | ||
| import java.time.LocalDateTime | ||
|
|
||
| data class MemberProfileResponse( | ||
| val memberId: Long, | ||
| val name: String?, | ||
| val nickname: String?, | ||
| val gender: Gender?, | ||
| val birthYear: Int?, | ||
|
cfcromn marked this conversation as resolved.
Outdated
|
||
| val profileImageUrl: String?, | ||
| val bio: String?, | ||
| val friendStatus: FriendStatus, | ||
| val createdAt: LocalDateTime, | ||
| ) { | ||
| companion object { | ||
| fun of(member: Member, friendStatus: FriendStatus) = MemberProfileResponse( | ||
| memberId = requireNotNull(member.id), | ||
| name = member.name, | ||
| nickname = member.nickname, | ||
| gender = member.gender, | ||
| birthYear = member.birthYear, | ||
| profileImageUrl = member.profileImageUrl, | ||
| bio = member.bio, | ||
| friendStatus = friendStatus, | ||
| createdAt = member.createdAt, | ||
| ) | ||
| } | ||
| } | ||
34 changes: 34 additions & 0 deletions
34
src/main/kotlin/team/cklob/mudda/domain/member/presentation/response/MyMemberResponse.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,34 @@ | ||
| package team.cklob.mudda.domain.member.presentation.response | ||
|
|
||
| import team.cklob.mudda.domain.member.domain.entity.Member | ||
| import team.cklob.mudda.domain.member.domain.type.Gender | ||
| import team.cklob.mudda.domain.member.domain.type.ProfileVisibility | ||
| import java.time.LocalDateTime | ||
|
|
||
| data class MyMemberResponse( | ||
| val memberId: Long, | ||
| val name: String?, | ||
| val nickname: String?, | ||
| val gender: Gender?, | ||
| val birthYear: Int?, | ||
| val profileImageUrl: String?, | ||
| val bio: String?, | ||
| val profileVisibility: ProfileVisibility, | ||
| val createdAt: LocalDateTime, | ||
| val updatedAt: LocalDateTime, | ||
| ) { | ||
| companion object { | ||
| fun from(member: Member) = MyMemberResponse( | ||
| memberId = requireNotNull(member.id), | ||
| name = member.name, | ||
| nickname = member.nickname, | ||
| gender = member.gender, | ||
| birthYear = member.birthYear, | ||
| profileImageUrl = member.profileImageUrl, | ||
| bio = member.bio, | ||
| profileVisibility = member.profileVisibility, | ||
| createdAt = member.createdAt, | ||
| updatedAt = member.updatedAt, | ||
| ) | ||
| } | ||
| } |
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
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
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.