From b3d72ead1f0c53a9439e56758b3b1581ea04d633 Mon Sep 17 00:00:00 2001 From: Klaus Date: Wed, 2 Aug 2023 14:04:31 +0900 Subject: [PATCH] =?UTF-8?q?=EB=A9=94=EC=8B=9C=EC=A7=80=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../sodalive/live/room/LiveRoomController.kt | 15 +- .../room/visit/LiveRoomVisitRepository.kt | 16 ++ .../live/room/visit/LiveRoomVisitService.kt | 19 +- .../sodalive/member/MemberController.kt | 10 + .../sodalive/member/MemberRepository.kt | 13 + .../sodalive/member/MemberService.kt | 15 ++ .../sodalive/message/GetMessageResponse.kt | 35 +++ .../co/vividnext/sodalive/message/Message.kt | 37 +++ .../sodalive/message/MessageController.kt | 136 ++++++++++ .../sodalive/message/MessageRepository.kt | 146 +++++++++++ .../sodalive/message/MessageService.kt | 240 ++++++++++++++++++ .../sodalive/message/SendMessageRequest.kt | 5 + 12 files changed, 685 insertions(+), 2 deletions(-) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/GetMessageResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/Message.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/MessageController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/MessageRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/message/SendMessageRequest.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt index 4f7fd58..6d8daac 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomController.kt @@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.live.room.cancel.CancelLiveRequest import kr.co.vividnext.sodalive.live.room.donation.DeleteLiveRoomDonationMessage import kr.co.vividnext.sodalive.live.room.donation.LiveRoomDonationRequest +import kr.co.vividnext.sodalive.live.room.visit.LiveRoomVisitService import kr.co.vividnext.sodalive.member.Member import org.springframework.data.domain.Pageable import org.springframework.security.core.annotation.AuthenticationPrincipal @@ -22,7 +23,10 @@ import org.springframework.web.multipart.MultipartFile @RestController @RequestMapping("/live/room") -class LiveRoomController(private val service: LiveRoomService) { +class LiveRoomController( + private val service: LiveRoomService, + private val visitService: LiveRoomVisitService +) { @GetMapping fun getRoomList( @@ -229,4 +233,13 @@ class LiveRoomController(private val service: LiveRoomService) { if (member == null) throw SodaException("로그인 정보를 확인해주세요.") ApiResponse.ok(service.quitRoom(roomId, member)) } + + @GetMapping("/recent_visit_room/users") + fun recentVisitRoomUsers( + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(visitService.getRecentVisitRoomUsers(member.id!!)) + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt index b90ce96..bd0f32b 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitRepository.kt @@ -3,6 +3,8 @@ package kr.co.vividnext.sodalive.live.room.visit import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom import kr.co.vividnext.sodalive.live.room.visit.QLiveRoomVisit.liveRoomVisit +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRole import kr.co.vividnext.sodalive.member.QMember.member import org.springframework.data.jpa.repository.JpaRepository import org.springframework.stereotype.Repository @@ -13,6 +15,7 @@ interface LiveRoomVisitRepository : JpaRepository, LiveRoom interface LiveRoomVisitQueryRepository { fun findByRoomIdAndMemberId(roomId: Long, memberId: Long): LiveRoomVisit? fun findFirstByMemberIdOrderByUpdatedAtDesc(memberId: Long): LiveRoomVisit? + fun getRecentVisitRoomUsers(roomId: Long, memberId: Long): List } @Repository @@ -38,4 +41,17 @@ class LiveRoomVisitQueryRepositoryImpl(private val queryFactory: JPAQueryFactory .orderBy(liveRoomVisit.updatedAt.desc()) .fetchFirst() } + + override fun getRecentVisitRoomUsers(roomId: Long, memberId: Long): List { + return queryFactory + .selectFrom(member) + .where( + liveRoomVisit.room.id.eq(roomId) + .and(liveRoomVisit.member.isActive.isTrue) + .and(liveRoomVisit.member.id.ne(memberId)) + .and(liveRoomVisit.member.role.ne(MemberRole.ADMIN)) + .and(liveRoomVisit.member.role.ne(MemberRole.AGENT)) + ) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt index df18ba1..0e89fd0 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/live/room/visit/LiveRoomVisitService.kt @@ -1,14 +1,21 @@ package kr.co.vividnext.sodalive.live.room.visit import kr.co.vividnext.sodalive.live.room.LiveRoom +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.member.Member +import org.springframework.beans.factory.annotation.Value import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional import java.time.LocalDateTime @Service @Transactional(readOnly = true) -class LiveRoomVisitService(private val repository: LiveRoomVisitRepository) { +class LiveRoomVisitService( + private val repository: LiveRoomVisitRepository, + + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { @Transactional fun roomVisit(room: LiveRoom, member: Member) { var roomVisit = repository.findByRoomIdAndMemberId(room.id!!, member.id!!) @@ -22,4 +29,14 @@ class LiveRoomVisitService(private val repository: LiveRoomVisitRepository) { repository.save(roomVisit) } + + fun getRecentVisitRoomUsers(memberId: Long): List { + val roomVisit = repository.findFirstByMemberIdOrderByUpdatedAtDesc(memberId) + ?: return emptyList() + + return repository.getRecentVisitRoomUsers(roomVisit.room!!.id!!, memberId) + .asSequence() + .map { GetRoomDetailUser(it, cloudFrontHost) } + .toList() + } } 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 89d996d..5979817 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberController.kt @@ -114,4 +114,14 @@ class MemberController(private val service: MemberService) { ApiResponse.ok(service.memberUnBlock(request = request, memberId = member.id!!)) } + + @GetMapping("/search") + fun searchMember( + @RequestParam nickname: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.searchMember(nickname = nickname, memberId = member.id!!)) + } } 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 011d73a..7edb17f 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt @@ -13,6 +13,7 @@ interface MemberRepository : JpaRepository, MemberQueryRepository interface MemberQueryRepository { fun findByPushToken(pushToken: String): List + fun findByNicknameAndOtherCondition(nickname: String, memberId: Long): List } @Repository @@ -23,4 +24,16 @@ class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Mem .where(member.pushToken.eq(pushToken)) .fetch() } + + override fun findByNicknameAndOtherCondition(nickname: String, memberId: Long): List { + return queryFactory + .selectFrom(member) + .where( + member.nickname.containsIgnoreCase(nickname) + .and(member.id.ne(memberId)) + .and(member.role.ne(MemberRole.ADMIN)) + .and(member.role.ne(MemberRole.AGENT)) + ) + .fetch() + } } 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 d2cd91e..b8d7bfd 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt @@ -6,6 +6,7 @@ import kr.co.vividnext.sodalive.aws.s3.S3Uploader import kr.co.vividnext.sodalive.common.ApiResponse import kr.co.vividnext.sodalive.common.SodaException import kr.co.vividnext.sodalive.jwt.TokenProvider +import kr.co.vividnext.sodalive.live.room.detail.GetRoomDetailUser import kr.co.vividnext.sodalive.member.block.BlockMember import kr.co.vividnext.sodalive.member.block.BlockMemberRepository import kr.co.vividnext.sodalive.member.block.MemberBlockRequest @@ -334,4 +335,18 @@ class MemberService( } fun isBlocked(blockedMemberId: Long, memberId: Long) = blockMemberRepository.isBlocked(blockedMemberId, memberId) + + fun searchMember(nickname: String, memberId: Long): List { + if (nickname.length < 2) { + throw SodaException("두 글자 이상 입력 하셔야 합니다.") + } + + return repository.findByNicknameAndOtherCondition(nickname, memberId) + .asSequence() + .filter { blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = it.id!!) } + .map { + GetRoomDetailUser(it, cloudFrontHost) + } + .toList() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/GetMessageResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/GetMessageResponse.kt new file mode 100644 index 0000000..dfa3efb --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/GetMessageResponse.kt @@ -0,0 +1,35 @@ +package kr.co.vividnext.sodalive.message + +data class GetVoiceMessageResponse( + val totalCount: Int, + val items: List +) { + data class VoiceMessageItem( + val messageId: Long, + val senderId: Long, + val senderNickname: String, + val senderProfileImageUrl: String, + val recipientNickname: String, + val recipientProfileImageUrl: String, + val voiceMessageUrl: String, + val date: String, + val isKept: Boolean + ) +} + +data class GetTextMessageResponse( + val totalCount: Int, + val items: List +) { + data class TextMessageItem( + val messageId: Long, + val senderId: Long, + val senderNickname: String, + val senderProfileImageUrl: String, + val recipientNickname: String, + val recipientProfileImageUrl: String, + val textMessage: String, + val date: String, + val isKept: Boolean + ) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/Message.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/Message.kt new file mode 100644 index 0000000..35fed97 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/Message.kt @@ -0,0 +1,37 @@ +package kr.co.vividnext.sodalive.message + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Column +import javax.persistence.Entity +import javax.persistence.EnumType +import javax.persistence.Enumerated +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class Message( + @Column(columnDefinition = "TEXT") + val textMessage: String? = null, + var voiceMessage: String? = null, + + @Enumerated(EnumType.STRING) + val messageType: MessageType = MessageType.TEXT, + + var isRecipientKeep: Boolean = false, + var isSenderDelete: Boolean = false, + var isRecipientDelete: Boolean = false +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "sender_id", nullable = false) + var sender: Member? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "recipient_id", nullable = false) + var recipient: Member? = null +} + +enum class MessageType { + TEXT, VOICE +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageController.kt new file mode 100644 index 0000000..1cb13b3 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageController.kt @@ -0,0 +1,136 @@ +package kr.co.vividnext.sodalive.message + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +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.PutMapping +import org.springframework.web.bind.annotation.RequestBody +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RequestPart +import org.springframework.web.bind.annotation.RestController +import org.springframework.web.multipart.MultipartFile + +@RestController +@RequestMapping("/message") +class MessageController(private val service: MessageService) { + @PostMapping("/send/text") + fun sendTextMessage( + @RequestBody request: SendTextMessageRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.sendTextMessage(request, member)) + } + + @GetMapping("/sent/text") + fun getSentTextMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getSentTextMessages(member, pageable, timezone)) + } + + @GetMapping("/received/text") + fun getReceivedTextMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getReceivedTextMessages(member, pageable, timezone)) + } + + @GetMapping("/keep/text") + fun getKeepTextMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getKeepTextMessages(member, pageable, timezone)) + } + + @PutMapping("/keep/text/{id}") + fun keepTextMessage( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.keepTextMessage(id, member)) + } + + @PostMapping("/send/voice") + fun sendVoiceMessage( + @RequestPart("voiceMessageFile") voiceMessageFile: MultipartFile, + @RequestPart("request") requestString: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.sendVoiceMessage(voiceMessageFile, requestString, member)) + } + + @GetMapping("/sent/voice") + fun getSentVoiceMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.getSentVoiceMessages(member, pageable, timezone)) + } + + @GetMapping("/received/voice") + fun getReceivedVoiceMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getReceivedVoiceMessages(member, pageable, timezone)) + } + + @GetMapping("/keep/voice") + fun getKeepVoiceMessages( + @RequestParam("timezone") timezone: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getKeepVoiceMessages(member, pageable, timezone)) + } + + @PutMapping("/keep/voice/{id}") + fun keepVoiceMessage( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + ApiResponse.ok(service.keepVoiceMessage(id, member)) + } + + @DeleteMapping("/{messageId}") + fun deleteMessage( + @PathVariable messageId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.deleteMessage(messageId, member)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageRepository.kt new file mode 100644 index 0000000..8e08125 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageRepository.kt @@ -0,0 +1,146 @@ +package kr.co.vividnext.sodalive.message + +import com.querydsl.core.types.dsl.BooleanExpression +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.message.QMessage.message +import org.springframework.data.domain.Pageable +import org.springframework.data.jpa.repository.JpaRepository +import org.springframework.stereotype.Repository + +@Repository +interface MessageRepository : JpaRepository, MessageQueryRepository + +interface MessageQueryRepository { + fun getSentTextMessageCount(memberId: Long): Int + fun getSentTextMessageList(pageable: Pageable, memberId: Long): List + fun getReceivedTextMessageCount(memberId: Long): Int + fun getReceivedTextMessageList(pageable: Pageable, memberId: Long): List + fun getKeepTextMessageCount(memberId: Long): Int + fun getKeepTextMessageList(pageable: Pageable, memberId: Long): List + fun getSentVoiceMessageCount(memberId: Long): Int + fun getSentVoiceMessageList(pageable: Pageable, memberId: Long): List + fun getReceivedVoiceMessageCount(memberId: Long): Int + fun getReceivedVoiceMessageList(pageable: Pageable, memberId: Long): List + fun getKeepVoiceMessageCount(memberId: Long): Int + fun getKeepVoiceMessageList(pageable: Pageable, memberId: Long): List +} + +@Repository +class MessageQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : MessageQueryRepository { + override fun getSentTextMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isSenderDelete.isFalse) + .and(message.sender.id.eq(memberId)) + + return totalCount(where) + } + + override fun getSentTextMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isSenderDelete.isFalse) + .and(message.sender.id.eq(memberId)) + + return messageList(pageable, where) + } + + override fun getReceivedTextMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isRecipientDelete.isFalse) + .and(message.recipient.id.eq(memberId)) + + return totalCount(where) + } + + override fun getReceivedTextMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isRecipientDelete.isFalse) + .and(message.recipient.id.eq(memberId)) + + return messageList(pageable, where) + } + + override fun getKeepTextMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isRecipientDelete.isFalse) + .and(message.isRecipientKeep.isTrue) + .and(message.recipient.id.eq(memberId)) + + return totalCount(where) + } + + override fun getKeepTextMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.TEXT) + .and(message.isRecipientDelete.isFalse) + .and(message.isRecipientKeep.isTrue) + .and(message.recipient.id.eq(memberId)) + + return messageList(pageable, where) + } + + override fun getSentVoiceMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isSenderDelete.isFalse) + .and(message.sender.id.eq(memberId)) + + return totalCount(where) + } + + override fun getSentVoiceMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isSenderDelete.isFalse) + .and(message.sender.id.eq(memberId)) + + return messageList(pageable, where) + } + + override fun getReceivedVoiceMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isRecipientDelete.isFalse) + .and(message.recipient.id.eq(memberId)) + + return totalCount(where) + } + + override fun getReceivedVoiceMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isRecipientDelete.isFalse) + .and(message.recipient.id.eq(memberId)) + + return messageList(pageable, where) + } + + override fun getKeepVoiceMessageCount(memberId: Long): Int { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isRecipientDelete.isFalse) + .and(message.isRecipientKeep.isTrue) + .and(message.recipient.id.eq(memberId)) + + return totalCount(where) + } + + override fun getKeepVoiceMessageList(pageable: Pageable, memberId: Long): List { + val where = message.messageType.eq(MessageType.VOICE) + .and(message.isRecipientDelete.isFalse) + .and(message.isRecipientKeep.isTrue) + .and(message.recipient.id.eq(memberId)) + + return messageList(pageable, where) + } + + private fun messageList(pageable: Pageable, where: BooleanExpression): List { + return queryFactory.selectFrom(message) + .offset(pageable.offset) + .limit(pageable.pageSize.toLong()) + .where(where) + .orderBy(message.id.desc()) + .fetch() + } + + private fun totalCount(where: BooleanExpression): Int { + return queryFactory.select(message.id) + .from(message) + .where(where) + .fetch() + .size + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt new file mode 100644 index 0000000..a6e3ec5 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt @@ -0,0 +1,240 @@ +package kr.co.vividnext.sodalive.message + +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.common.SodaException +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.MemberRepository +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.data.domain.Pageable +import org.springframework.data.repository.findByIdOrNull +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile +import java.time.ZoneId +import java.time.format.DateTimeFormatter + +@Service +@Transactional(readOnly = true) +class MessageService( + private val repository: MessageRepository, + private val memberRepository: MemberRepository, + private val blockMemberRepository: BlockMemberRepository, + + private val objectMapper: ObjectMapper, + private val s3Uploader: S3Uploader, + + @Value("\${cloud.aws.s3.bucket}") + private val bucket: String, + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) { + @Transactional + fun sendTextMessage(request: SendTextMessageRequest, member: Member) { + val recipient = memberRepository.findByIdOrNull(request.recipientId) + ?: throw SodaException("받는 사람이 없습니다.") + + if (!recipient.isActive) { + throw SodaException("탈퇴한 유저에게는 메시지를 보내실 수 없습니다.") + } + + val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = request.recipientId) + if (isBlocked) throw SodaException("${recipient.nickname}님의 요청으로 메시지를 보낼 수 없습니다.") + + val sender = memberRepository.findByIdOrNull(member.id!!) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + val message = Message( + textMessage = request.textMessage, + messageType = MessageType.TEXT + ) + + message.sender = sender + message.recipient = recipient + + repository.save(message) + } + + fun getSentTextMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { + val totalCount = repository.getSentTextMessageCount(memberId = member.id!!) + val messageList = repository.getSentTextMessageList(pageable, memberId = member.id!!) + return getTextMessageResponse(totalCount, messageList, timezone) + } + + fun getReceivedTextMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { + val totalCount = repository.getReceivedTextMessageCount(memberId = member.id!!) + val messageList = repository.getReceivedTextMessageList(pageable, memberId = member.id!!) + return getTextMessageResponse(totalCount, messageList, timezone) + } + + fun getKeepTextMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { + val totalCount = repository.getKeepTextMessageCount(memberId = member.id!!) + val messageList = repository.getKeepTextMessageList(pageable, memberId = member.id!!) + return getTextMessageResponse(totalCount, messageList, timezone) + } + + @Transactional + fun keepTextMessage(messageId: Long, member: Member) { + keepMessage(messageId, member) + } + + @Transactional + fun sendVoiceMessage(voiceMessageFile: MultipartFile, requestString: String, member: Member) { + val request = objectMapper.readValue(requestString, SendVoiceMessageRequest::class.java) + + val recipient = memberRepository.findByIdOrNull(request.recipientId) + ?: throw SodaException("받는 사람이 없습니다.") + + if (!recipient.isActive) { + throw SodaException("탈퇴한 유저에게는 메시지를 보내실 수 없습니다.") + } + + val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = member.id!!, memberId = request.recipientId) + if (isBlocked) throw SodaException("${recipient.nickname}님의 요청으로 메시지를 보낼 수 없습니다.") + + val sender = memberRepository.findByIdOrNull(member.id!!) + ?: throw SodaException("로그인 정보를 확인해주세요.") + + val message = Message(messageType = MessageType.VOICE) + message.sender = sender + message.recipient = recipient + + repository.save(message) + + val metadata = ObjectMetadata() + metadata.contentLength = voiceMessageFile.size + + val messagePath = s3Uploader.upload( + inputStream = voiceMessageFile.inputStream, + bucket = bucket, + filePath = "voice_message/${message.id}/${generateFileName(prefix = "${message.id}-message-")}", + metadata = metadata + ) + + message.voiceMessage = messagePath + } + + fun getSentVoiceMessages(member: Member, pageable: Pageable, timezone: String): GetVoiceMessageResponse { + val totalCount = repository.getSentVoiceMessageCount(memberId = member.id!!) + val messageList = repository.getSentVoiceMessageList(pageable, memberId = member.id!!) + + return getVoiceMessageResponse(totalCount, messageList, timezone) + } + + fun getReceivedVoiceMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { + val totalCount = repository.getReceivedVoiceMessageCount(memberId = member.id!!) + val messageList = repository.getReceivedVoiceMessageList(pageable, memberId = member.id!!) + return getTextMessageResponse(totalCount, messageList, timezone) + } + + fun getKeepVoiceMessages(member: Member, pageable: Pageable, timezone: String): GetTextMessageResponse { + val totalCount = repository.getKeepVoiceMessageCount(memberId = member.id!!) + val messageList = repository.getKeepVoiceMessageList(pageable, memberId = member.id!!) + return getTextMessageResponse(totalCount, messageList, timezone) + } + + @Transactional + fun keepVoiceMessage(messageId: Long, member: Member) { + keepMessage(messageId, member) + } + + @Transactional + fun deleteMessage(messageId: Long, member: Member) { + val message = repository.findByIdOrNull(messageId) + ?: throw SodaException("해당하는 메시지가 없습니다.\n다시 확인해 주시기 바랍니다.") + + if (message.sender!!.id!! == member.id!!) { + message.isSenderDelete = true + } else if (message.recipient!!.id!! == member.id!!) { + message.isRecipientDelete = true + } + } + + private fun getTextMessageResponse( + totalCount: Int, + messageList: List, + timezone: String + ) = GetTextMessageResponse( + totalCount = totalCount, + items = messageList.asSequence() + .map { + val createdAt = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetTextMessageResponse.TextMessageItem( + messageId = it.id!!, + senderId = it.sender!!.id!!, + senderNickname = it.sender!!.nickname, + senderProfileImageUrl = if (it.sender!!.profileImage != null) { + "$cloudFrontHost/${it.sender!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + recipientNickname = it.recipient!!.nickname, + recipientProfileImageUrl = if (it.recipient!!.profileImage != null) { + "$cloudFrontHost/${it.recipient!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + textMessage = it.textMessage!!, + date = createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + isKept = false + ) + } + .toList() + ) + + private fun getVoiceMessageResponse( + totalCount: Int, + messageList: List, + timezone: String + ) = GetVoiceMessageResponse( + totalCount = totalCount, + items = messageList.asSequence() + .map { + val createdAt = it.createdAt!! + .atZone(ZoneId.of("UTC")) + .withZoneSameInstant(ZoneId.of(timezone)) + + GetVoiceMessageResponse.VoiceMessageItem( + messageId = it.id!!, + senderId = it.sender!!.id!!, + senderNickname = it.sender!!.nickname, + senderProfileImageUrl = if (it.sender!!.profileImage != null) { + "$cloudFrontHost/${it.sender!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + recipientNickname = it.recipient!!.nickname, + recipientProfileImageUrl = if (it.recipient!!.profileImage != null) { + "$cloudFrontHost/${it.recipient!!.profileImage}" + } else { + "$cloudFrontHost/profile/default-profile.png" + }, + voiceMessageUrl = "$cloudFrontHost/${it.voiceMessage!!}", + date = createdAt.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")), + isKept = it.isRecipientKeep + ) + } + .toList() + ) + + private fun keepMessage(messageId: Long, member: Member) { + val message = repository.findByIdOrNull(messageId) + ?: throw SodaException("잘못된 요청입니다.") + + if (message.recipient != member) { + throw SodaException("잘못된 요청입니다.") + } + + if (message.isRecipientKeep) { + throw SodaException("이미 보관된 메시지 입니다.") + } + + message.isRecipientKeep = true + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/message/SendMessageRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/message/SendMessageRequest.kt new file mode 100644 index 0000000..2afe5f5 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/message/SendMessageRequest.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.message + +data class SendVoiceMessageRequest(val recipientId: Long, val container: String) + +data class SendTextMessageRequest(val recipientId: Long, val textMessage: String, val container: String)