diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt index d29aae7..e3da095 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -340,6 +340,7 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) ) ) .from(audioContent) + .innerJoin(audioContent.member, member) .where(where) .orderBy(audioContent.id.desc()) .fetch() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/GetChangeNicknamePriceResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/GetChangeNicknamePriceResponse.kt new file mode 100644 index 0000000..f839164 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/GetChangeNicknamePriceResponse.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.member + +data class GetChangeNicknamePriceResponse(val price: Int) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt index a515312..cc3128d 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -22,6 +22,18 @@ import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/member") class MemberController(private val service: MemberService) { + @GetMapping("/check/email") + fun checkEmail(@RequestParam email: String) = service.duplicateCheckEmail(email) + + @GetMapping("/check/nickname") + fun checkNickname(@RequestParam nickname: String) = service.duplicateCheckNickname(nickname) + + @PutMapping("/change/nickname") + fun changeNickname( + @RequestBody profileUpdateRequest: ProfileUpdateRequest, + @AuthenticationPrincipal user: User + ) = ApiResponse.ok(service.updateNickname(profileUpdateRequest, user)) + @PostMapping("/signup") fun signUp( @RequestPart("profileImage", required = false) profileImage: MultipartFile? = null, @@ -50,6 +62,16 @@ class MemberController(private val service: MemberService) { ApiResponse.ok(service.logoutAll(member.id!!)) } + @GetMapping + fun getMember( + @RequestParam container: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getMember(member.id!!, container)) + } + @GetMapping("/info") fun getMemberInfo( @RequestParam container: String?, @@ -151,4 +173,24 @@ class MemberController(private val service: MemberService) { @RequestBody signOutRequest: SignOutRequest, @AuthenticationPrincipal user: User ) = ApiResponse.ok(service.signOut(signOutRequest, user), "정상적으로 탈퇴 처리되었습니다.") + + @GetMapping("/change/nickname/price") + fun getChangeNicknamePrice( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getChangeNicknamePrice(memberId = member.id!!)) + } + + @PutMapping + fun profileUpdate( + @RequestBody profileUpdateRequest: ProfileUpdateRequest, + @AuthenticationPrincipal user: User + ) = ApiResponse.ok(service.profileUpdate(profileUpdateRequest, user)) + + @PostMapping("/image") + fun profileImageUpdate( + @RequestParam("image") multipartFile: MultipartFile, + @AuthenticationPrincipal user: User + ) = ApiResponse.ok(service.profileImageUpdate(multipartFile, user)) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt index 615774c..32a07e6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -7,6 +7,7 @@ import kr.co.vividnext.sodalive.member.QMember.member import kr.co.vividnext.sodalive.member.auth.QAuth.auth import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.member.following.QCreatorFollowing.creatorFollowing +import kr.co.vividnext.sodalive.member.nickname.QNicknameChangeLog.nicknameChangeLog import kr.co.vividnext.sodalive.member.notification.QMemberNotification.memberNotification import kr.co.vividnext.sodalive.message.QMessage.message import org.springframework.data.jpa.repository.JpaRepository @@ -37,6 +38,7 @@ interface MemberQueryRepository { fun getMessageRecipientPushToken(messageId: Long): GetMessageRecipientPushTokenResponse fun getIndividualRecipientPushTokens(recipients: List, isAuth: Boolean): Map>> + fun getChangeNicknamePrice(memberId: Long): GetChangeNicknamePriceResponse } @Repository @@ -210,4 +212,21 @@ class MemberQueryRepositoryImpl( return mapOf("aos" to aosPushTokens, "ios" to iosPushTokens) } + + override fun getChangeNicknamePrice(memberId: Long): GetChangeNicknamePriceResponse { + val changeCount = queryFactory + .select(nicknameChangeLog.id) + .from(nicknameChangeLog) + .where(nicknameChangeLog.member.id.eq(memberId)) + .fetch() + .count() + + return GetChangeNicknamePriceResponse( + price = if (changeCount > 0) { + 1000 + } else { + 0 + } + ) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt index a916159..922d022 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.member import com.amazonaws.services.s3.model.ObjectMetadata import com.fasterxml.jackson.databind.ObjectMapper import kr.co.vividnext.sodalive.aws.s3.S3Uploader +import kr.co.vividnext.sodalive.can.payment.CanPaymentService +import kr.co.vividnext.sodalive.can.use.CanUsage import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.jwt.TokenProvider @@ -16,6 +18,8 @@ import kr.co.vividnext.sodalive.member.info.GetMemberInfoResponse import kr.co.vividnext.sodalive.member.login.LoginRequest import kr.co.vividnext.sodalive.member.login.LoginResponse import kr.co.vividnext.sodalive.member.myPage.MyPageResponse +import kr.co.vividnext.sodalive.member.nickname.NicknameChangeLog +import kr.co.vividnext.sodalive.member.nickname.NicknameChangeLogRepository import kr.co.vividnext.sodalive.member.notification.MemberNotificationService import kr.co.vividnext.sodalive.member.notification.UpdateNotificationSettingRequest import kr.co.vividnext.sodalive.member.signUp.SignUpRequest @@ -25,6 +29,8 @@ import kr.co.vividnext.sodalive.member.stipulation.StipulationAgree import kr.co.vividnext.sodalive.member.stipulation.StipulationAgreeRepository import kr.co.vividnext.sodalive.member.stipulation.StipulationIds import kr.co.vividnext.sodalive.member.stipulation.StipulationRepository +import kr.co.vividnext.sodalive.member.tag.MemberCreatorTag +import kr.co.vividnext.sodalive.member.tag.MemberTagRepository import kr.co.vividnext.sodalive.member.token.MemberTokenRepository import kr.co.vividnext.sodalive.utils.generateFileName import org.springframework.beans.factory.annotation.Value @@ -53,7 +59,10 @@ class MemberService( private val creatorFollowingRepository: CreatorFollowingRepository, private val blockMemberRepository: BlockMemberRepository, private val signOutRepository: SignOutRepository, + private val nicknameChangeLogRepository: NicknameChangeLogRepository, + private val memberTagRepository: MemberTagRepository, + private val canPaymentService: CanPaymentService, private val memberNotificationService: MemberNotificationService, private val s3Uploader: S3Uploader, @@ -107,6 +116,13 @@ class MemberService( ) } + fun getMember(id: Long, container: String): ProfileResponse { + val member = repository.findByIdOrNull(id) + ?: throw SodaException("없는 사용자 입니다.") + + return ProfileResponse(member, cloudFrontHost, container) + } + fun getMemberInfo(member: Member, container: String): GetMemberInfoResponse { return GetMemberInfoResponse( can = member.getChargeCan(container) + member.getRewardCan(container), @@ -406,6 +422,130 @@ class MemberService( signOutRepository.save(signOut) } + fun getChangeNicknamePrice(memberId: Long): GetChangeNicknamePriceResponse { + return repository.getChangeNicknamePrice(memberId = memberId) + } + + @Transactional + fun updateNickname(profileUpdateRequest: ProfileUpdateRequest, user: User) { + if (profileUpdateRequest.email != user.username) { + throw SodaException("로그인 정보를 확인해 주세요.") + } + + val member = repository.findByEmail(user.username) ?: throw SodaException("로그인 정보를 확인해주세요.") + + if (profileUpdateRequest.nickname != null) { + validateNickname(profileUpdateRequest.nickname) + repository.findByNickname(profileUpdateRequest.nickname) + ?.let { throw SodaException("이미 사용중인 닉네임 입니다.") } + + val price = repository.getChangeNicknamePrice(memberId = member.id!!).price + if (price > 0) { + canPaymentService.spendCan( + memberId = member.id!!, + needCan = price, + canUsage = CanUsage.CHANGE_NICKNAME, + container = profileUpdateRequest.container + ) + } + + val nicknameChangeLog = NicknameChangeLog(prevNickname = member.nickname) + nicknameChangeLog.member = member + nicknameChangeLogRepository.save(nicknameChangeLog) + + member.nickname = profileUpdateRequest.nickname + } + } + + @Transactional + fun profileUpdate(profileUpdateRequest: ProfileUpdateRequest, user: User): ProfileResponse { + if (profileUpdateRequest.email != user.username) { + throw SodaException("로그인 정보를 확인해 주세요.") + } + + val member = repository.findByEmail(user.username) ?: throw SodaException("로그인 정보를 확인해주세요.") + + if (profileUpdateRequest.modifyPassword != null) { + if (passwordEncoder.matches(profileUpdateRequest.password, member.password)) { + validatePassword(profileUpdateRequest.modifyPassword) + member.password = passwordEncoder.encode(profileUpdateRequest.modifyPassword) + } else { + throw SodaException("비밀번호가 일치하지 않습니다.") + } + } + + if (profileUpdateRequest.gender != null) { + member.gender = profileUpdateRequest.gender + } + + if (profileUpdateRequest.nickname != null) { + validateNickname(profileUpdateRequest.nickname) + repository.findByNickname(profileUpdateRequest.nickname) + ?.let { throw SodaException("이미 사용중인 닉네임 입니다.") } + member.nickname = profileUpdateRequest.nickname + } + + val tags = if (!profileUpdateRequest.removeTags.isNullOrEmpty()) { + member.tags.filter { !profileUpdateRequest.removeTags.contains(it.tag.tag) } + } else { + member.tags + }.toMutableList() + + if (!profileUpdateRequest.insertTags.isNullOrEmpty()) { + val accountCounselorTags = memberTagRepository.findByMember(member).map { it.tag } + profileUpdateRequest.insertTags.forEach { + val tag = memberTagRepository.findByTag(it) + if (tag != null && !accountCounselorTags.contains(tag)) { + tags.add(MemberCreatorTag(member, tag)) + } + } + } + + if (tags != member.tags) { + member.tags.clear() + member.tags.addAll(tags) + } + + if (profileUpdateRequest.introduce != null) { + member.introduce = profileUpdateRequest.introduce + } + + if (profileUpdateRequest.youtubeUrl != null) { + member.youtubeUrl = profileUpdateRequest.youtubeUrl + } + + if (profileUpdateRequest.instagramUrl != null) { + member.instagramUrl = profileUpdateRequest.instagramUrl + } + + if (profileUpdateRequest.websiteUrl != null) { + member.websiteUrl = profileUpdateRequest.websiteUrl + } + + if (profileUpdateRequest.blogUrl != null) { + member.blogUrl = profileUpdateRequest.blogUrl + } + + return ProfileResponse(member, cloudFrontHost, profileUpdateRequest.container) + } + + @Transactional + fun profileImageUpdate(multipartFile: MultipartFile, user: User): String { + val member = repository.findByEmail(user.username) ?: throw SodaException("로그인 정보를 확인해주세요.") + + val metadata = ObjectMetadata() + metadata.contentLength = multipartFile.size + + member.profileImage = s3Uploader.upload( + inputStream = multipartFile.inputStream, + bucket = s3Bucket, + filePath = "profile/${member.id}/${generateFileName(prefix = "${member.id}-profile")}", + metadata = metadata + ) + + return "$cloudFrontHost/${member.profileImage!!}" + } + private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock { return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/ProfileResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/ProfileResponse.kt new file mode 100644 index 0000000..17ad822 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/ProfileResponse.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.member + +data class ProfileResponse( + val userId: Long, + val email: String, + val nickname: String, + val gender: Gender, + val profileUrl: String, + val chargeCan: Int, + val rewardCan: Int, + val youtubeUrl: String?, + val instagramUrl: String?, + val blogUrl: String?, + val websiteUrl: String?, + val introduce: String, + val tags: List +) { + constructor(member: Member, cloudFrontHost: String, container: String) : this( + userId = member.id!!, + email = member.email, + nickname = member.nickname, + gender = member.gender, + profileUrl = if (member.profileImage != null) { + "$cloudFrontHost/${member.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + chargeCan = member.getChargeCan(container), + rewardCan = member.getRewardCan(container), + youtubeUrl = member.youtubeUrl, + instagramUrl = member.instagramUrl, + websiteUrl = member.websiteUrl, + blogUrl = member.blogUrl, + introduce = member.introduce, + tags = member.tags.asSequence().filter { it.tag.isActive }.map { it.tag.tag }.toList() + ) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/ProfileUpdateRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/ProfileUpdateRequest.kt new file mode 100644 index 0000000..790db10 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/ProfileUpdateRequest.kt @@ -0,0 +1,17 @@ +package kr.co.vividnext.sodalive.member + +data class ProfileUpdateRequest( + val email: String, + val password: String? = null, + val modifyPassword: String? = null, + val nickname: String? = null, + val gender: Gender? = null, + val insertTags: List? = null, + val removeTags: List? = null, + val introduce: String? = null, + val youtubeUrl: String? = null, + val instagramUrl: String? = null, + val websiteUrl: String? = null, + val blogUrl: String? = null, + val container: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameChangeLog.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameChangeLog.kt new file mode 100644 index 0000000..6af084e --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameChangeLog.kt @@ -0,0 +1,30 @@ +package kr.co.vividnext.sodalive.member.nickname + +import kr.co.vividnext.sodalive.member.Member +import java.time.LocalDateTime +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.PrePersist + +@Entity +data class NicknameChangeLog( + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null, + val prevNickname: String, + var createdAt: LocalDateTime? = null +) { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameChangeLogRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameChangeLogRepository.kt new file mode 100644 index 0000000..2ec5313 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/nickname/NicknameChangeLogRepository.kt @@ -0,0 +1,7 @@ +package kr.co.vividnext.sodalive.member.nickname + +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface NicknameChangeLogRepository : JpaRepository diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt index 078a252..43e3035 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/tag/MemberTagRepository.kt @@ -1,7 +1,9 @@ package kr.co.vividnext.sodalive.member.tag import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.member.Member import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag +import kr.co.vividnext.sodalive.member.tag.QMemberCreatorTag.memberCreatorTag import org.springframework.beans.factory.annotation.Value import org.springframework.data.jpa.repository.JpaRepository @@ -11,6 +13,7 @@ interface MemberTagRepository : JpaRepository, MemberTagQueryR interface MemberTagQueryRepository { fun getTags(): List + fun findByMember(member: Member): List } class MemberTagQueryRepositoryImpl( @@ -33,4 +36,11 @@ class MemberTagQueryRepositoryImpl( .orderBy(creatorTag.orders.asc()) .fetch() } + + override fun findByMember(member: Member): List { + return queryFactory + .selectFrom(memberCreatorTag) + .where(memberCreatorTag.member.id.eq(member.id)) + .fetch() + } }