feat(original): 원작
- 원천 원작, 원천 원작 링크, 글/그림 작가, 제작사, 태그 추가
This commit is contained in:
parent
edeecad2ce
commit
67186bba55
|
@ -12,7 +12,12 @@ data class OriginalWorkRegisterRequest(
|
||||||
@JsonProperty("category") val category: String,
|
@JsonProperty("category") val category: String,
|
||||||
@JsonProperty("isAdult") val isAdult: Boolean = false,
|
@JsonProperty("isAdult") val isAdult: Boolean = false,
|
||||||
@JsonProperty("description") val description: String = "",
|
@JsonProperty("description") val description: String = "",
|
||||||
@JsonProperty("originalLink") val originalLink: String? = null
|
@JsonProperty("originalWork") val originalWork: String? = null,
|
||||||
|
@JsonProperty("originalLink") val originalLink: String? = null,
|
||||||
|
@JsonProperty("writer") val writer: String? = null,
|
||||||
|
@JsonProperty("studio") val studio: String? = null,
|
||||||
|
@JsonProperty("originalLinks") val originalLinks: List<String>? = null,
|
||||||
|
@JsonProperty("tags") val tags: List<String>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -25,7 +30,12 @@ data class OriginalWorkUpdateRequest(
|
||||||
@JsonProperty("category") val category: String? = null,
|
@JsonProperty("category") val category: String? = null,
|
||||||
@JsonProperty("isAdult") val isAdult: Boolean? = null,
|
@JsonProperty("isAdult") val isAdult: Boolean? = null,
|
||||||
@JsonProperty("description") val description: String? = null,
|
@JsonProperty("description") val description: String? = null,
|
||||||
@JsonProperty("originalLink") val originalLink: String? = null
|
@JsonProperty("originalWork") val originalWork: String? = null,
|
||||||
|
@JsonProperty("originalLink") val originalLink: String? = null,
|
||||||
|
@JsonProperty("writer") val writer: String? = null,
|
||||||
|
@JsonProperty("studio") val studio: String? = null,
|
||||||
|
@JsonProperty("originalLinks") val originalLinks: List<String>? = null,
|
||||||
|
@JsonProperty("tags") val tags: List<String>? = null
|
||||||
)
|
)
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -38,7 +48,12 @@ data class OriginalWorkResponse(
|
||||||
val category: String,
|
val category: String,
|
||||||
val isAdult: Boolean,
|
val isAdult: Boolean,
|
||||||
val description: String,
|
val description: String,
|
||||||
|
val originalWork: String?,
|
||||||
val originalLink: String?,
|
val originalLink: String?,
|
||||||
|
val writer: String?,
|
||||||
|
val studio: String?,
|
||||||
|
val originalLinks: List<String>,
|
||||||
|
val tags: List<String>,
|
||||||
val imageUrl: String?
|
val imageUrl: String?
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -55,7 +70,12 @@ data class OriginalWorkResponse(
|
||||||
category = entity.category,
|
category = entity.category,
|
||||||
isAdult = entity.isAdult,
|
isAdult = entity.isAdult,
|
||||||
description = entity.description,
|
description = entity.description,
|
||||||
|
originalWork = entity.originalWork,
|
||||||
originalLink = entity.originalLink,
|
originalLink = entity.originalLink,
|
||||||
|
writer = entity.writer,
|
||||||
|
studio = entity.studio,
|
||||||
|
originalLinks = entity.originalLinks.map { it.url },
|
||||||
|
tags = entity.tagMappings.map { it.tag.tag },
|
||||||
imageUrl = fullImagePath
|
imageUrl = fullImagePath
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,9 @@ import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||||
import kr.co.vividnext.sodalive.chat.original.OriginalWork
|
import kr.co.vividnext.sodalive.chat.original.OriginalWork
|
||||||
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkRepository
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTag
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTagMapping
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.repository.OriginalWorkTagRepository
|
||||||
import kr.co.vividnext.sodalive.common.SodaException
|
import kr.co.vividnext.sodalive.common.SodaException
|
||||||
import org.springframework.data.domain.Page
|
import org.springframework.data.domain.Page
|
||||||
import org.springframework.data.domain.PageRequest
|
import org.springframework.data.domain.PageRequest
|
||||||
|
@ -20,7 +23,8 @@ import org.springframework.transaction.annotation.Transactional
|
||||||
@Service
|
@Service
|
||||||
class AdminOriginalWorkService(
|
class AdminOriginalWorkService(
|
||||||
private val originalWorkRepository: OriginalWorkRepository,
|
private val originalWorkRepository: OriginalWorkRepository,
|
||||||
private val chatCharacterRepository: ChatCharacterRepository
|
private val chatCharacterRepository: ChatCharacterRepository,
|
||||||
|
private val originalWorkTagRepository: OriginalWorkTagRepository
|
||||||
) {
|
) {
|
||||||
|
|
||||||
/** 원작 등록 (중복 제목 방지 포함) */
|
/** 원작 등록 (중복 제목 방지 포함) */
|
||||||
|
@ -35,8 +39,23 @@ class AdminOriginalWorkService(
|
||||||
category = request.category,
|
category = request.category,
|
||||||
isAdult = request.isAdult,
|
isAdult = request.isAdult,
|
||||||
description = request.description,
|
description = request.description,
|
||||||
originalLink = request.originalLink
|
originalWork = request.originalWork,
|
||||||
|
originalLink = request.originalLink,
|
||||||
|
writer = request.writer,
|
||||||
|
studio = request.studio
|
||||||
)
|
)
|
||||||
|
// 링크 리스트 생성
|
||||||
|
request.originalLinks?.filter { it.isNotBlank() }?.forEach { link ->
|
||||||
|
entity.originalLinks.add(kr.co.vividnext.sodalive.chat.original.OriginalWorkLink(url = link, originalWork = entity))
|
||||||
|
}
|
||||||
|
// 태그 매핑 생성 (기존 태그 재사용)
|
||||||
|
request.tags?.let { tags ->
|
||||||
|
val normalized = tags.map { it.trim() }.filter { it.isNotBlank() }.toSet()
|
||||||
|
normalized.forEach { t ->
|
||||||
|
val tagEntity = originalWorkTagRepository.findByTag(t) ?: originalWorkTagRepository.save(OriginalWorkTag(t))
|
||||||
|
entity.tagMappings.add(OriginalWorkTagMapping(originalWork = entity, tag = tagEntity))
|
||||||
|
}
|
||||||
|
}
|
||||||
return originalWorkRepository.save(entity)
|
return originalWorkRepository.save(entity)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -51,7 +70,40 @@ class AdminOriginalWorkService(
|
||||||
request.category?.let { ow.category = it }
|
request.category?.let { ow.category = it }
|
||||||
request.isAdult?.let { ow.isAdult = it }
|
request.isAdult?.let { ow.isAdult = it }
|
||||||
request.description?.let { ow.description = it }
|
request.description?.let { ow.description = it }
|
||||||
|
request.originalWork?.let { ow.originalWork = it }
|
||||||
request.originalLink?.let { ow.originalLink = it }
|
request.originalLink?.let { ow.originalLink = it }
|
||||||
|
request.writer?.let { ow.writer = it }
|
||||||
|
request.studio?.let { ow.studio = it }
|
||||||
|
// 링크 리스트가 전달되면 기존 것을 교체
|
||||||
|
request.originalLinks?.let { links ->
|
||||||
|
ow.originalLinks.clear()
|
||||||
|
links.filter { it.isNotBlank() }.forEach { link ->
|
||||||
|
ow.originalLinks.add(kr.co.vividnext.sodalive.chat.original.OriginalWorkLink(url = link, originalWork = ow))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// 태그 변경사항만 반영 (요청이 null이면 변경 없음)
|
||||||
|
request.tags?.let { tags ->
|
||||||
|
val normalized = tags.map { it.trim() }.filter { it.isNotBlank() }.toSet()
|
||||||
|
val current = ow.tagMappings.map { it.tag.tag }.toSet()
|
||||||
|
val toAdd = normalized.minus(current)
|
||||||
|
val toRemove = current.minus(normalized)
|
||||||
|
|
||||||
|
if (toRemove.isNotEmpty()) {
|
||||||
|
val itr = ow.tagMappings.iterator()
|
||||||
|
while (itr.hasNext()) {
|
||||||
|
val m = itr.next()
|
||||||
|
if (toRemove.contains(m.tag.tag)) {
|
||||||
|
itr.remove() // orphanRemoval=true로 매핑 삭제
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (toAdd.isNotEmpty()) {
|
||||||
|
toAdd.forEach { t ->
|
||||||
|
val tagEntity = originalWorkTagRepository.findByTag(t) ?: originalWorkTagRepository.save(OriginalWorkTag(t))
|
||||||
|
ow.tagMappings.add(OriginalWorkTagMapping(originalWork = ow, tag = tagEntity))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
if (imagePath != null) {
|
if (imagePath != null) {
|
||||||
ow.imagePath = imagePath
|
ow.imagePath = imagePath
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,8 +1,10 @@
|
||||||
package kr.co.vividnext.sodalive.chat.original
|
package kr.co.vividnext.sodalive.chat.original
|
||||||
|
|
||||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.CascadeType
|
||||||
import javax.persistence.Column
|
import javax.persistence.Column
|
||||||
import javax.persistence.Entity
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.OneToMany
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 원작(오리지널 작품) 엔티티
|
* 원작(오리지널 작품) 엔티티
|
||||||
|
@ -31,13 +33,33 @@ class OriginalWork(
|
||||||
@Column(columnDefinition = "TEXT")
|
@Column(columnDefinition = "TEXT")
|
||||||
var description: String = "",
|
var description: String = "",
|
||||||
|
|
||||||
/** 원작 링크 */
|
/** 원천 원작 */
|
||||||
@Column(nullable = true)
|
@Column(nullable = true)
|
||||||
var originalLink: String? = null
|
var originalWork: String? = null,
|
||||||
|
|
||||||
|
/** 원천 원작 링크(단일) */
|
||||||
|
@Column(nullable = true)
|
||||||
|
var originalLink: String? = null,
|
||||||
|
|
||||||
|
/** 작가 */
|
||||||
|
@Column(nullable = true)
|
||||||
|
var writer: String? = null,
|
||||||
|
|
||||||
|
/** 제작사 */
|
||||||
|
@Column(nullable = true)
|
||||||
|
var studio: String? = null
|
||||||
) : BaseEntity() {
|
) : BaseEntity() {
|
||||||
/** 원작 대표 이미지 S3 경로 */
|
/** 원작 대표 이미지 S3 경로 */
|
||||||
var imagePath: String? = null
|
var imagePath: String? = null
|
||||||
|
|
||||||
/** 소프트 삭제 여부 (true면 삭제된 것으로 간주) */
|
/** 소프트 삭제 여부 (true면 삭제된 것으로 간주) */
|
||||||
var isDeleted: Boolean = false
|
var isDeleted: Boolean = false
|
||||||
|
|
||||||
|
/** 원작 링크들 (1:N) */
|
||||||
|
@OneToMany(mappedBy = "originalWork", cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||||
|
var originalLinks: MutableList<OriginalWorkLink> = mutableListOf()
|
||||||
|
|
||||||
|
/** 원작 태그 매핑들 (1:N) */
|
||||||
|
@OneToMany(mappedBy = "originalWork", cascade = [CascadeType.ALL], orphanRemoval = true)
|
||||||
|
var tagMappings: MutableList<OriginalWorkTagMapping> = mutableListOf()
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,22 @@
|
||||||
|
package kr.co.vividnext.sodalive.chat.original
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 원작 원본 링크 엔티티
|
||||||
|
* - 하나의 원작(OriginalWork)에 여러 개의 링크가 연결될 수 있음 (1:N)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
class OriginalWorkLink(
|
||||||
|
@Column(nullable = false)
|
||||||
|
var url: String,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "original_work_id")
|
||||||
|
var originalWork: OriginalWork? = null
|
||||||
|
) : BaseEntity()
|
|
@ -0,0 +1,21 @@
|
||||||
|
package kr.co.vividnext.sodalive.chat.original
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.Column
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.OneToMany
|
||||||
|
import javax.persistence.Table
|
||||||
|
import javax.persistence.UniqueConstraint
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 원작 태그 엔티티 (작품/시리즈 태그와 분리)
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
@Table(uniqueConstraints = [UniqueConstraint(columnNames = ["tag"])])
|
||||||
|
class OriginalWorkTag(
|
||||||
|
@Column(nullable = false)
|
||||||
|
val tag: String
|
||||||
|
) : BaseEntity() {
|
||||||
|
@OneToMany(mappedBy = "tag")
|
||||||
|
var tagMappings: MutableList<OriginalWorkTagMapping> = mutableListOf()
|
||||||
|
}
|
|
@ -0,0 +1,21 @@
|
||||||
|
package kr.co.vividnext.sodalive.chat.original
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||||
|
import javax.persistence.Entity
|
||||||
|
import javax.persistence.FetchType
|
||||||
|
import javax.persistence.JoinColumn
|
||||||
|
import javax.persistence.ManyToOne
|
||||||
|
|
||||||
|
/**
|
||||||
|
* OriginalWork 와 OriginalWorkTag 매핑 엔티티
|
||||||
|
*/
|
||||||
|
@Entity
|
||||||
|
class OriginalWorkTagMapping(
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "original_work_id")
|
||||||
|
val originalWork: OriginalWork,
|
||||||
|
|
||||||
|
@ManyToOne(fetch = FetchType.LAZY)
|
||||||
|
@JoinColumn(name = "tag_id")
|
||||||
|
val tag: OriginalWorkTag
|
||||||
|
) : BaseEntity()
|
|
@ -48,7 +48,12 @@ data class OriginalWorkDetailResponse(
|
||||||
@JsonProperty("category") val category: String,
|
@JsonProperty("category") val category: String,
|
||||||
@JsonProperty("isAdult") val isAdult: Boolean,
|
@JsonProperty("isAdult") val isAdult: Boolean,
|
||||||
@JsonProperty("description") val description: String,
|
@JsonProperty("description") val description: String,
|
||||||
|
@JsonProperty("originalWork") val originalWork: String?,
|
||||||
@JsonProperty("originalLink") val originalLink: String?,
|
@JsonProperty("originalLink") val originalLink: String?,
|
||||||
|
@JsonProperty("writer") val writer: String?,
|
||||||
|
@JsonProperty("studio") val studio: String?,
|
||||||
|
@JsonProperty("originalLinks") val originalLinks: List<String>,
|
||||||
|
@JsonProperty("tags") val tags: List<String>,
|
||||||
@JsonProperty("characters") val characters: List<Character>
|
@JsonProperty("characters") val characters: List<Character>
|
||||||
) {
|
) {
|
||||||
companion object {
|
companion object {
|
||||||
|
@ -69,7 +74,12 @@ data class OriginalWorkDetailResponse(
|
||||||
category = entity.category,
|
category = entity.category,
|
||||||
isAdult = entity.isAdult,
|
isAdult = entity.isAdult,
|
||||||
description = entity.description,
|
description = entity.description,
|
||||||
|
originalWork = entity.originalWork,
|
||||||
originalLink = entity.originalLink,
|
originalLink = entity.originalLink,
|
||||||
|
writer = entity.writer,
|
||||||
|
studio = entity.studio,
|
||||||
|
originalLinks = entity.originalLinks.map { it.url },
|
||||||
|
tags = entity.tagMappings.map { it.tag.tag },
|
||||||
characters = characters
|
characters = characters
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,10 @@
|
||||||
|
package kr.co.vividnext.sodalive.chat.original.repository
|
||||||
|
|
||||||
|
import kr.co.vividnext.sodalive.chat.original.OriginalWorkTag
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository
|
||||||
|
import org.springframework.stereotype.Repository
|
||||||
|
|
||||||
|
@Repository
|
||||||
|
interface OriginalWorkTagRepository : JpaRepository<OriginalWorkTag, Long> {
|
||||||
|
fun findByTag(tag: String): OriginalWorkTag?
|
||||||
|
}
|
Loading…
Reference in New Issue