test #343

Merged
klaus merged 3 commits from test into main 2025-09-18 19:25:50 +00:00
8 changed files with 184 additions and 6 deletions
Showing only changes of commit 67186bba55 - Show all commits

View File

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

View File

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

View File

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

View File

@ -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()

View File

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

View File

@ -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()

View File

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

View File

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