test #343
|
@ -3,6 +3,7 @@ package kr.co.vividnext.sodalive.admin.chat.character
|
|||
import com.amazonaws.services.s3.model.ObjectMetadata
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterRegisterRequest
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchListPageResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterUpdateRequest
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ExternalApiResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.service.AdminChatCharacterService
|
||||
|
@ -73,14 +74,21 @@ class AdminChatCharacterController(
|
|||
/**
|
||||
* 캐릭터 검색(관리자)
|
||||
* - 이름/설명/MBTI/태그 기준 부분 검색, 활성 캐릭터만 대상
|
||||
* - 페이징 제거: 전체 목록 반환
|
||||
* - 페이징 지원: page, size 파라미터 사용
|
||||
*/
|
||||
@GetMapping("/search")
|
||||
fun searchCharacters(
|
||||
@RequestParam("searchTerm") searchTerm: String
|
||||
@RequestParam("searchTerm") searchTerm: String,
|
||||
@RequestParam(defaultValue = "0") page: Int,
|
||||
@RequestParam(defaultValue = "20") size: Int
|
||||
) = run {
|
||||
val list = adminService.searchCharactersAll(searchTerm, imageHost)
|
||||
ApiResponse.ok(list)
|
||||
val pageable = adminService.createDefaultPageRequest(page, size)
|
||||
val resultPage = adminService.searchCharacters(searchTerm, pageable, imageHost)
|
||||
val response = ChatCharacterSearchListPageResponse(
|
||||
totalCount = resultPage.totalElements,
|
||||
content = resultPage.content
|
||||
)
|
||||
ApiResponse.ok(response)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -0,0 +1,9 @@
|
|||
package kr.co.vividnext.sodalive.admin.chat.character.dto
|
||||
|
||||
/**
|
||||
* 캐릭터 검색 결과 페이지 응답 DTO
|
||||
*/
|
||||
data class ChatCharacterSearchListPageResponse(
|
||||
val totalCount: Long,
|
||||
val content: List<ChatCharacterListResponse>
|
||||
)
|
|
@ -3,16 +3,16 @@ package kr.co.vividnext.sodalive.admin.chat.character.dto
|
|||
import kr.co.vividnext.sodalive.chat.character.ChatCharacter
|
||||
|
||||
/**
|
||||
* 캐릭터 검색 결과 응답 DTO
|
||||
* 원작 연결된 캐릭터 결과 응답 DTO
|
||||
*/
|
||||
data class ChatCharacterSearchResponse(
|
||||
data class OriginalWorkChatCharacterResponse(
|
||||
val id: Long,
|
||||
val name: String,
|
||||
val imagePath: String?
|
||||
) {
|
||||
companion object {
|
||||
fun from(character: ChatCharacter, imageHost: String): ChatCharacterSearchResponse {
|
||||
return ChatCharacterSearchResponse(
|
||||
fun from(character: ChatCharacter, imageHost: String): OriginalWorkChatCharacterResponse {
|
||||
return OriginalWorkChatCharacterResponse(
|
||||
id = character.id!!,
|
||||
name = character.name,
|
||||
imagePath = character.imagePath?.let { "$imageHost/$it" }
|
||||
|
@ -22,9 +22,9 @@ data class ChatCharacterSearchResponse(
|
|||
}
|
||||
|
||||
/**
|
||||
* 캐릭터 검색 결과 페이지 응답 DTO
|
||||
* 원작 연결된 캐릭터 결과 페이지 응답 DTO
|
||||
*/
|
||||
data class ChatCharacterSearchListPageResponse(
|
||||
data class OriginalWorkChatCharacterListPageResponse(
|
||||
val totalCount: Long,
|
||||
val content: List<ChatCharacterSearchResponse>
|
||||
val content: List<OriginalWorkChatCharacterResponse>
|
||||
)
|
|
@ -3,7 +3,6 @@ package kr.co.vividnext.sodalive.admin.chat.character.service
|
|||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterDetailResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListPageResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterListResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchResponse
|
||||
import kr.co.vividnext.sodalive.chat.character.repository.ChatCharacterRepository
|
||||
import kr.co.vividnext.sodalive.common.SodaException
|
||||
import org.springframework.data.domain.Page
|
||||
|
@ -72,24 +71,8 @@ class AdminChatCharacterService(
|
|||
searchTerm: String,
|
||||
pageable: Pageable,
|
||||
imageHost: String = ""
|
||||
): Page<ChatCharacterSearchResponse> {
|
||||
): Page<ChatCharacterListResponse> {
|
||||
val characters = chatCharacterRepository.searchCharacters(searchTerm, pageable)
|
||||
return characters.map { ChatCharacterSearchResponse.from(it, imageHost) }
|
||||
}
|
||||
|
||||
/**
|
||||
* 캐릭터 검색 (이름, 설명, MBTI, 태그 기반) - 무페이징
|
||||
*
|
||||
* @param searchTerm 검색어
|
||||
* @param imageHost 이미지 호스트 URL
|
||||
* @return 검색된 캐릭터 목록 (전체)
|
||||
*/
|
||||
@Transactional(readOnly = true)
|
||||
fun searchCharactersAll(
|
||||
searchTerm: String,
|
||||
imageHost: String = ""
|
||||
): List<ChatCharacterSearchResponse> {
|
||||
val characters = chatCharacterRepository.searchCharactersNoPaging(searchTerm)
|
||||
return characters.map { ChatCharacterSearchResponse.from(it, imageHost) }
|
||||
return characters.map { ChatCharacterListResponse.from(it, imageHost) }
|
||||
}
|
||||
}
|
||||
|
|
|
@ -2,8 +2,8 @@ package kr.co.vividnext.sodalive.admin.chat.original
|
|||
|
||||
import com.amazonaws.services.s3.model.ObjectMetadata
|
||||
import com.fasterxml.jackson.databind.ObjectMapper
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchListPageResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.ChatCharacterSearchResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.OriginalWorkChatCharacterListPageResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.character.dto.OriginalWorkChatCharacterResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkAssignCharactersRequest
|
||||
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkPageResponse
|
||||
import kr.co.vividnext.sodalive.admin.chat.original.dto.OriginalWorkRegisterRequest
|
||||
|
@ -172,9 +172,9 @@ class AdminOriginalWorkController(
|
|||
@RequestParam(defaultValue = "20") size: Int
|
||||
) = run {
|
||||
val pageRes = originalWorkService.getCharactersOfOriginalWorkPage(id, page, size)
|
||||
val content = pageRes.content.map { ChatCharacterSearchResponse.from(it, imageHost) }
|
||||
val content = pageRes.content.map { OriginalWorkChatCharacterResponse.from(it, imageHost) }
|
||||
ApiResponse.ok(
|
||||
ChatCharacterSearchListPageResponse(
|
||||
OriginalWorkChatCharacterListPageResponse(
|
||||
totalCount = pageRes.totalElements,
|
||||
content = content
|
||||
)
|
||||
|
|
|
@ -12,7 +12,12 @@ data class OriginalWorkRegisterRequest(
|
|||
@JsonProperty("category") val category: String,
|
||||
@JsonProperty("isAdult") val isAdult: Boolean = false,
|
||||
@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("isAdult") val isAdult: Boolean? = 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 isAdult: Boolean,
|
||||
val description: String,
|
||||
val originalWork: String?,
|
||||
val originalLink: String?,
|
||||
val writer: String?,
|
||||
val studio: String?,
|
||||
val originalLinks: List<String>,
|
||||
val tags: List<String>,
|
||||
val imageUrl: String?
|
||||
) {
|
||||
companion object {
|
||||
|
@ -55,7 +70,12 @@ data class OriginalWorkResponse(
|
|||
category = entity.category,
|
||||
isAdult = entity.isAdult,
|
||||
description = entity.description,
|
||||
originalWork = entity.originalWork,
|
||||
originalLink = entity.originalLink,
|
||||
writer = entity.writer,
|
||||
studio = entity.studio,
|
||||
originalLinks = entity.originalLinks.map { it.url },
|
||||
tags = entity.tagMappings.map { it.tag.tag },
|
||||
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.original.OriginalWork
|
||||
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 org.springframework.data.domain.Page
|
||||
import org.springframework.data.domain.PageRequest
|
||||
|
@ -20,7 +23,8 @@ import org.springframework.transaction.annotation.Transactional
|
|||
@Service
|
||||
class AdminOriginalWorkService(
|
||||
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,
|
||||
isAdult = request.isAdult,
|
||||
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)
|
||||
}
|
||||
|
||||
|
@ -51,7 +70,40 @@ class AdminOriginalWorkService(
|
|||
request.category?.let { ow.category = it }
|
||||
request.isAdult?.let { ow.isAdult = it }
|
||||
request.description?.let { ow.description = it }
|
||||
request.originalWork?.let { ow.originalWork = 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) {
|
||||
ow.imagePath = imagePath
|
||||
}
|
||||
|
|
|
@ -12,7 +12,6 @@ import org.springframework.stereotype.Repository
|
|||
interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
|
||||
fun findByName(name: String): ChatCharacter?
|
||||
fun findByIsActiveTrue(pageable: Pageable): Page<ChatCharacter>
|
||||
fun findByOriginalWorkIdAndIsActiveTrueOrderByCreatedAtDesc(originalWorkId: Long): List<ChatCharacter>
|
||||
fun findByOriginalWorkIdAndIsActiveTrue(originalWorkId: Long, pageable: Pageable): Page<ChatCharacter>
|
||||
|
||||
/**
|
||||
|
@ -54,28 +53,6 @@ interface ChatCharacterRepository : JpaRepository<ChatCharacter, Long> {
|
|||
pageable: Pageable
|
||||
): Page<ChatCharacter>
|
||||
|
||||
/**
|
||||
* 이름, 설명, MBTI, 태그로 캐릭터 검색 - 무페이징 전체 목록
|
||||
*/
|
||||
@Query(
|
||||
"""
|
||||
SELECT DISTINCT c FROM ChatCharacter c
|
||||
LEFT JOIN c.tagMappings tm
|
||||
LEFT JOIN tm.tag t
|
||||
WHERE c.isActive = true AND
|
||||
(
|
||||
LOWER(c.name) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR
|
||||
LOWER(c.description) LIKE LOWER(CONCAT('%', :searchTerm, '%')) OR
|
||||
(c.mbti IS NOT NULL AND LOWER(c.mbti) LIKE LOWER(CONCAT('%', :searchTerm, '%'))) OR
|
||||
(t.tag IS NOT NULL AND LOWER(t.tag) LIKE LOWER(CONCAT('%', :searchTerm, '%')))
|
||||
)
|
||||
ORDER BY c.createdAt DESC
|
||||
"""
|
||||
)
|
||||
fun searchCharactersNoPaging(
|
||||
@Param("searchTerm") searchTerm: String
|
||||
): List<ChatCharacter>
|
||||
|
||||
/**
|
||||
* 특정 캐릭터와 태그를 공유하는 다른 캐릭터를 무작위로 조회 (현재 캐릭터 제외)
|
||||
*/
|
||||
|
|
|
@ -1,8 +1,10 @@
|
|||
package kr.co.vividnext.sodalive.chat.original
|
||||
|
||||
import kr.co.vividnext.sodalive.common.BaseEntity
|
||||
import javax.persistence.CascadeType
|
||||
import javax.persistence.Column
|
||||
import javax.persistence.Entity
|
||||
import javax.persistence.OneToMany
|
||||
|
||||
/**
|
||||
* 원작(오리지널 작품) 엔티티
|
||||
|
@ -31,13 +33,33 @@ class OriginalWork(
|
|||
@Column(columnDefinition = "TEXT")
|
||||
var description: String = "",
|
||||
|
||||
/** 원작 링크 */
|
||||
/** 원천 원작 */
|
||||
@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() {
|
||||
/** 원작 대표 이미지 S3 경로 */
|
||||
var imagePath: String? = null
|
||||
|
||||
/** 소프트 삭제 여부 (true면 삭제된 것으로 간주) */
|
||||
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("isAdult") val isAdult: Boolean,
|
||||
@JsonProperty("description") val description: String,
|
||||
@JsonProperty("originalWork") val originalWork: 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>
|
||||
) {
|
||||
companion object {
|
||||
|
@ -69,7 +74,12 @@ data class OriginalWorkDetailResponse(
|
|||
category = entity.category,
|
||||
isAdult = entity.isAdult,
|
||||
description = entity.description,
|
||||
originalWork = entity.originalWork,
|
||||
originalLink = entity.originalLink,
|
||||
writer = entity.writer,
|
||||
studio = entity.studio,
|
||||
originalLinks = entity.originalLinks.map { it.url },
|
||||
tags = entity.tagMappings.map { it.tag.tag },
|
||||
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