Compare commits
4 Commits
f61c45e89a
...
1c0d40aed9
Author | SHA1 | Date | |
---|---|---|---|
1c0d40aed9 | |||
1444afaae2 | |||
a05bc369b7 | |||
6c7f411869 |
@@ -5,6 +5,7 @@ import kr.co.vividnext.sodalive.common.SodaException
|
|||||||
import kr.co.vividnext.sodalive.member.Member
|
import kr.co.vividnext.sodalive.member.Member
|
||||||
import org.springframework.beans.factory.annotation.Value
|
import org.springframework.beans.factory.annotation.Value
|
||||||
import org.springframework.security.core.annotation.AuthenticationPrincipal
|
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.GetMapping
|
||||||
import org.springframework.web.bind.annotation.PathVariable
|
import org.springframework.web.bind.annotation.PathVariable
|
||||||
import org.springframework.web.bind.annotation.PostMapping
|
import org.springframework.web.bind.annotation.PostMapping
|
||||||
@@ -54,12 +55,13 @@ class CharacterCommentController(
|
|||||||
fun listComments(
|
fun listComments(
|
||||||
@PathVariable characterId: Long,
|
@PathVariable characterId: Long,
|
||||||
@RequestParam(required = false, defaultValue = "20") limit: Int,
|
@RequestParam(required = false, defaultValue = "20") limit: Int,
|
||||||
|
@RequestParam(required = false) cursor: Long?,
|
||||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
) = run {
|
) = run {
|
||||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||||
|
|
||||||
val data = service.listComments(imageHost, characterId, limit)
|
val data = service.listComments(imageHost, characterId, cursor, limit)
|
||||||
ApiResponse.ok(data)
|
ApiResponse.ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -68,13 +70,39 @@ class CharacterCommentController(
|
|||||||
@PathVariable characterId: Long,
|
@PathVariable characterId: Long,
|
||||||
@PathVariable commentId: Long,
|
@PathVariable commentId: Long,
|
||||||
@RequestParam(required = false, defaultValue = "20") limit: Int,
|
@RequestParam(required = false, defaultValue = "20") limit: Int,
|
||||||
|
@RequestParam(required = false) cursor: Long?,
|
||||||
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
) = run {
|
) = run {
|
||||||
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||||
|
|
||||||
// characterId는 서비스 내부 검증(원본 댓글과 캐릭터 일치)에서 검증됨
|
// characterId는 서비스 내부 검증(원본 댓글과 캐릭터 일치)에서 검증됨
|
||||||
val data = service.getReplies(imageHost, commentId, limit)
|
val data = service.getReplies(imageHost, commentId, cursor, limit)
|
||||||
ApiResponse.ok(data)
|
ApiResponse.ok(data)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@DeleteMapping("/{characterId}/comments/{commentId}")
|
||||||
|
fun deleteComment(
|
||||||
|
@PathVariable characterId: Long,
|
||||||
|
@PathVariable commentId: Long,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||||
|
service.deleteComment(characterId, commentId, member)
|
||||||
|
ApiResponse.ok(true, "댓글이 삭제되었습니다.")
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/{characterId}/comments/{commentId}/reports")
|
||||||
|
fun reportComment(
|
||||||
|
@PathVariable characterId: Long,
|
||||||
|
@PathVariable commentId: Long,
|
||||||
|
@RequestBody request: ReportCharacterCommentRequest,
|
||||||
|
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
|
||||||
|
) = run {
|
||||||
|
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
|
||||||
|
if (member.auth == null) throw SodaException("본인인증을 하셔야 합니다.")
|
||||||
|
service.reportComment(characterId, commentId, member, request.content)
|
||||||
|
ApiResponse.ok(true, "신고가 접수되었습니다.")
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -15,6 +15,7 @@ data class CreateCharacterCommentRequest(
|
|||||||
|
|
||||||
data class CharacterCommentResponse(
|
data class CharacterCommentResponse(
|
||||||
val commentId: Long,
|
val commentId: Long,
|
||||||
|
val memberId: Long,
|
||||||
val memberProfileImage: String,
|
val memberProfileImage: String,
|
||||||
val memberNickname: String,
|
val memberNickname: String,
|
||||||
val createdAt: Long,
|
val createdAt: Long,
|
||||||
@@ -30,6 +31,7 @@ data class CharacterCommentResponse(
|
|||||||
|
|
||||||
data class CharacterReplyResponse(
|
data class CharacterReplyResponse(
|
||||||
val replyId: Long,
|
val replyId: Long,
|
||||||
|
val memberId: Long,
|
||||||
val memberProfileImage: String,
|
val memberProfileImage: String,
|
||||||
val memberNickname: String,
|
val memberNickname: String,
|
||||||
val createdAt: Long
|
val createdAt: Long
|
||||||
@@ -41,5 +43,21 @@ data class CharacterReplyResponse(
|
|||||||
|
|
||||||
data class CharacterCommentRepliesResponse(
|
data class CharacterCommentRepliesResponse(
|
||||||
val original: CharacterCommentResponse,
|
val original: CharacterCommentResponse,
|
||||||
val replies: List<CharacterReplyResponse>
|
val replies: List<CharacterReplyResponse>,
|
||||||
|
val cursor: Long?
|
||||||
|
)
|
||||||
|
|
||||||
|
// 댓글 리스트 조회 Response 컨테이너
|
||||||
|
// - 전체 댓글 개수(totalCount)
|
||||||
|
// - 댓글 목록(comments)
|
||||||
|
|
||||||
|
data class CharacterCommentListResponse(
|
||||||
|
val totalCount: Int,
|
||||||
|
val comments: List<CharacterCommentResponse>,
|
||||||
|
val cursor: Long?
|
||||||
|
)
|
||||||
|
|
||||||
|
// 신고 Request
|
||||||
|
data class ReportCharacterCommentRequest(
|
||||||
|
val content: String
|
||||||
)
|
)
|
||||||
|
@@ -0,0 +1,25 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.character.comment
|
||||||
|
|
||||||
|
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.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
import javax.persistence.Table
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "character_comment_report")
|
||||||
|
data class CharacterCommentReport(
|
||||||
|
@Column(columnDefinition = "TEXT", nullable = false)
|
||||||
|
val content: String
|
||||||
|
) : BaseEntity() {
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "comment_id", nullable = false)
|
||||||
|
var comment: CharacterComment? = null
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "member_id", nullable = false)
|
||||||
|
var member: Member? = null
|
||||||
|
}
|
@@ -0,0 +1,5 @@
|
|||||||
|
package kr.co.vividnext.sodalive.chat.character.comment
|
||||||
|
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
|
||||||
|
interface CharacterCommentReportRepository : JpaRepository<CharacterCommentReport, Long>
|
@@ -9,8 +9,22 @@ interface CharacterCommentRepository : JpaRepository<CharacterComment, Long> {
|
|||||||
pageable: Pageable
|
pageable: Pageable
|
||||||
): List<CharacterComment>
|
): List<CharacterComment>
|
||||||
|
|
||||||
|
fun findByChatCharacter_IdAndIsActiveTrueAndParentIsNullAndIdLessThanOrderByCreatedAtDesc(
|
||||||
|
chatCharacterId: Long,
|
||||||
|
id: Long,
|
||||||
|
pageable: Pageable
|
||||||
|
): List<CharacterComment>
|
||||||
|
|
||||||
fun countByParent_IdAndIsActiveTrue(parentId: Long): Int
|
fun countByParent_IdAndIsActiveTrue(parentId: Long): Int
|
||||||
|
|
||||||
fun findByParent_IdAndIsActiveTrueOrderByCreatedAtDesc(parentId: Long, pageable: Pageable): List<CharacterComment>
|
fun findByParent_IdAndIsActiveTrueOrderByCreatedAtDesc(parentId: Long, pageable: Pageable): List<CharacterComment>
|
||||||
|
|
||||||
|
fun findByParent_IdAndIsActiveTrueAndIdLessThanOrderByCreatedAtDesc(
|
||||||
|
parentId: Long,
|
||||||
|
id: Long,
|
||||||
|
pageable: Pageable
|
||||||
|
): List<CharacterComment>
|
||||||
|
|
||||||
fun findFirstByChatCharacter_IdAndIsActiveTrueOrderByCreatedAtDesc(chatCharacterId: Long): CharacterComment?
|
fun findFirstByChatCharacter_IdAndIsActiveTrueOrderByCreatedAtDesc(chatCharacterId: Long): CharacterComment?
|
||||||
|
|
||||||
// 전체(상위+답글) 활성 댓글 총 개수
|
// 전체(상위+답글) 활성 댓글 총 개수
|
||||||
|
@@ -11,7 +11,8 @@ import java.time.ZoneId
|
|||||||
@Service
|
@Service
|
||||||
class CharacterCommentService(
|
class CharacterCommentService(
|
||||||
private val chatCharacterRepository: ChatCharacterRepository,
|
private val chatCharacterRepository: ChatCharacterRepository,
|
||||||
private val commentRepository: CharacterCommentRepository
|
private val commentRepository: CharacterCommentRepository,
|
||||||
|
private val reportRepository: CharacterCommentReportRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
private fun profileUrl(imageHost: String, profileImage: String?): String {
|
private fun profileUrl(imageHost: String, profileImage: String?): String {
|
||||||
@@ -34,6 +35,7 @@ class CharacterCommentService(
|
|||||||
val member = entity.member ?: throw SodaException("유효하지 않은 댓글입니다.")
|
val member = entity.member ?: throw SodaException("유효하지 않은 댓글입니다.")
|
||||||
return CharacterCommentResponse(
|
return CharacterCommentResponse(
|
||||||
commentId = entity.id!!,
|
commentId = entity.id!!,
|
||||||
|
memberId = member.id!!,
|
||||||
memberProfileImage = profileUrl(imageHost, member.profileImage),
|
memberProfileImage = profileUrl(imageHost, member.profileImage),
|
||||||
memberNickname = member.nickname,
|
memberNickname = member.nickname,
|
||||||
createdAt = toEpochMilli(entity.createdAt),
|
createdAt = toEpochMilli(entity.createdAt),
|
||||||
@@ -46,6 +48,7 @@ class CharacterCommentService(
|
|||||||
val member = entity.member ?: throw SodaException("유효하지 않은 댓글입니다.")
|
val member = entity.member ?: throw SodaException("유효하지 않은 댓글입니다.")
|
||||||
return CharacterReplyResponse(
|
return CharacterReplyResponse(
|
||||||
replyId = entity.id!!,
|
replyId = entity.id!!,
|
||||||
|
memberId = member.id!!,
|
||||||
memberProfileImage = profileUrl(imageHost, member.profileImage),
|
memberProfileImage = profileUrl(imageHost, member.profileImage),
|
||||||
memberNickname = member.nickname,
|
memberNickname = member.nickname,
|
||||||
createdAt = toEpochMilli(entity.createdAt)
|
createdAt = toEpochMilli(entity.createdAt)
|
||||||
@@ -83,26 +86,67 @@ class CharacterCommentService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun listComments(imageHost: String, characterId: Long, limit: Int = 20): List<CharacterCommentResponse> {
|
fun listComments(
|
||||||
|
imageHost: String,
|
||||||
|
characterId: Long,
|
||||||
|
cursor: Long?,
|
||||||
|
limit: Int = 20
|
||||||
|
): CharacterCommentListResponse {
|
||||||
val pageable = PageRequest.of(0, limit)
|
val pageable = PageRequest.of(0, limit)
|
||||||
val comments = commentRepository.findByChatCharacter_IdAndIsActiveTrueAndParentIsNullOrderByCreatedAtDesc(
|
val comments = if (cursor == null) {
|
||||||
|
commentRepository.findByChatCharacter_IdAndIsActiveTrueAndParentIsNullOrderByCreatedAtDesc(
|
||||||
characterId,
|
characterId,
|
||||||
pageable
|
pageable
|
||||||
)
|
)
|
||||||
return comments.map { toCommentResponse(imageHost, it) }
|
} else {
|
||||||
|
commentRepository.findByChatCharacter_IdAndIsActiveTrueAndParentIsNullAndIdLessThanOrderByCreatedAtDesc(
|
||||||
|
characterId,
|
||||||
|
cursor,
|
||||||
|
pageable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val items = comments.map { toCommentResponse(imageHost, it) }
|
||||||
|
val total = getTotalCommentCount(characterId)
|
||||||
|
val nextCursor = if (items.size == limit) items.lastOrNull()?.commentId else null
|
||||||
|
|
||||||
|
return CharacterCommentListResponse(
|
||||||
|
totalCount = total,
|
||||||
|
comments = items,
|
||||||
|
cursor = nextCursor
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@Transactional(readOnly = true)
|
@Transactional(readOnly = true)
|
||||||
fun getReplies(imageHost: String, commentId: Long, limit: Int = 20): CharacterCommentRepliesResponse {
|
fun getReplies(
|
||||||
val original = commentRepository.findById(commentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") }
|
imageHost: String,
|
||||||
|
commentId: Long,
|
||||||
|
cursor: Long?,
|
||||||
|
limit: Int = 20
|
||||||
|
): CharacterCommentRepliesResponse {
|
||||||
|
val original = commentRepository.findById(commentId).orElseThrow {
|
||||||
|
SodaException("댓글을 찾을 수 없습니다.")
|
||||||
|
}
|
||||||
if (!original.isActive) throw SodaException("비활성화된 댓글입니다.")
|
if (!original.isActive) throw SodaException("비활성화된 댓글입니다.")
|
||||||
|
|
||||||
val pageable = PageRequest.of(0, limit)
|
val pageable = PageRequest.of(0, limit)
|
||||||
val replies = commentRepository.findByParent_IdAndIsActiveTrueOrderByCreatedAtDesc(commentId, pageable)
|
val replies = if (cursor == null) {
|
||||||
|
commentRepository.findByParent_IdAndIsActiveTrueOrderByCreatedAtDesc(commentId, pageable)
|
||||||
|
} else {
|
||||||
|
commentRepository.findByParent_IdAndIsActiveTrueAndIdLessThanOrderByCreatedAtDesc(
|
||||||
|
commentId,
|
||||||
|
cursor,
|
||||||
|
pageable
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
val items = replies.map { toReplyResponse(imageHost, it) }
|
||||||
|
val nextCursor = if (items.size == limit) items.lastOrNull()?.replyId else null
|
||||||
|
|
||||||
return CharacterCommentRepliesResponse(
|
return CharacterCommentRepliesResponse(
|
||||||
original = toCommentResponse(imageHost, original, 0),
|
original = toCommentResponse(imageHost, original, 0),
|
||||||
replies = replies.map { toReplyResponse(imageHost, it) }
|
replies = items,
|
||||||
|
cursor = nextCursor
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -116,4 +160,27 @@ class CharacterCommentService(
|
|||||||
fun getTotalCommentCount(characterId: Long): Int {
|
fun getTotalCommentCount(characterId: Long): Int {
|
||||||
return commentRepository.countByChatCharacter_IdAndIsActiveTrue(characterId)
|
return commentRepository.countByChatCharacter_IdAndIsActiveTrue(characterId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun deleteComment(characterId: Long, commentId: Long, member: Member) {
|
||||||
|
val comment = commentRepository.findById(commentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") }
|
||||||
|
if (comment.chatCharacter?.id != characterId) throw SodaException("잘못된 요청입니다.")
|
||||||
|
if (!comment.isActive) return
|
||||||
|
val ownerId = comment.member?.id ?: throw SodaException("유효하지 않은 댓글입니다.")
|
||||||
|
if (ownerId != member.id) throw SodaException("삭제 권한이 없습니다.")
|
||||||
|
comment.isActive = false
|
||||||
|
commentRepository.save(comment)
|
||||||
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
fun reportComment(characterId: Long, commentId: Long, member: Member, content: String) {
|
||||||
|
val comment = commentRepository.findById(commentId).orElseThrow { SodaException("댓글을 찾을 수 없습니다.") }
|
||||||
|
if (comment.chatCharacter?.id != characterId) throw SodaException("잘못된 요청입니다.")
|
||||||
|
if (content.isBlank()) throw SodaException("신고 내용을 입력해주세요.")
|
||||||
|
|
||||||
|
val report = CharacterCommentReport(content = content)
|
||||||
|
report.comment = comment
|
||||||
|
report.member = member
|
||||||
|
reportRepository.save(report)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
Reference in New Issue
Block a user