feat(chat-character-comment): 캐릭터 댓글 삭제 및 신고 API 추가

- 삭제 API: 본인 댓글에 대해 soft delete 처리
- 신고 API: 신고 내용을 그대로 저장하는 CharacterCommentReport 엔티티/리포지토리 도입
- Controller: 삭제, 신고 엔드포인트 추가 및 인증/본인인증 체크
- Service: 비즈니스 로직 구현 및 예외 처리 강화

왜: 캐릭터 댓글 관리 기능 요구사항(삭제/신고)을 충족하기 위함
무엇: 엔드포인트, 서비스 로직, DTO 및 JPA 엔티티/리포지토리 추가
This commit is contained in:
Klaus 2025-08-20 00:13:13 +09:00
parent a05bc369b7
commit 1444afaae2
5 changed files with 86 additions and 1 deletions

View File

@ -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
@ -79,4 +80,29 @@ class CharacterCommentController(
val data = service.getReplies(imageHost, commentId, cursor, 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, "신고가 접수되었습니다.")
}
} }

View File

@ -54,3 +54,8 @@ data class CharacterCommentListResponse(
val comments: List<CharacterCommentResponse>, val comments: List<CharacterCommentResponse>,
val cursor: Long? val cursor: Long?
) )
// 신고 Request
data class ReportCharacterCommentRequest(
val content: String
)

View File

@ -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
}

View File

@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.chat.character.comment
import org.springframework.data.jpa.repository.JpaRepository
interface CharacterCommentReportRepository : JpaRepository<CharacterCommentReport, Long>

View File

@ -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 {
@ -157,4 +158,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)
}
} }