Compare commits

...

12 Commits

79 changed files with 2875 additions and 4 deletions

View File

@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.admin.can
data class AdminCanChargeRequest(
val memberId: Long,
val method: String,
val can: Int
)

View File

@ -0,0 +1,24 @@
package kr.co.vividnext.sodalive.admin.can
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/admin/can")
@PreAuthorize("hasRole('ADMIN')")
class AdminCanController(private val service: AdminCanService) {
@PostMapping
fun insertCan(@RequestBody request: AdminCanRequest) = ApiResponse.ok(service.saveCan(request))
@DeleteMapping("/{id}")
fun deleteCan(@PathVariable id: Long) = ApiResponse.ok(service.deleteCan(id))
@PostMapping("/charge")
fun charge(@RequestBody request: AdminCanChargeRequest) = ApiResponse.ok(service.charge(request))
}

View File

@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.admin.can
import kr.co.vividnext.sodalive.can.Can
import org.springframework.data.jpa.repository.JpaRepository
interface AdminCanRepository : JpaRepository<Can, Long>

View File

@ -0,0 +1,26 @@
package kr.co.vividnext.sodalive.admin.can
import kr.co.vividnext.sodalive.can.Can
import kr.co.vividnext.sodalive.can.CanStatus
import kr.co.vividnext.sodalive.extensions.moneyFormat
data class AdminCanRequest(
val can: Int,
val rewardCan: Int,
val price: Int
) {
fun toEntity(): Can {
var title = "${can.moneyFormat()}"
if (rewardCan > 0) {
title = "$title + ${rewardCan.moneyFormat()}"
}
return Can(
title = title,
can = can,
rewardCan = rewardCan,
price = price,
status = CanStatus.SALE
)
}
}

View File

@ -0,0 +1,56 @@
package kr.co.vividnext.sodalive.admin.can
import kr.co.vividnext.sodalive.admin.member.AdminMemberRepository
import kr.co.vividnext.sodalive.can.CanStatus
import kr.co.vividnext.sodalive.can.charge.Charge
import kr.co.vividnext.sodalive.can.charge.ChargeRepository
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.payment.Payment
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.extensions.moneyFormat
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class AdminCanService(
private val repository: AdminCanRepository,
private val chargeRepository: ChargeRepository,
private val memberRepository: AdminMemberRepository
) {
@Transactional
fun saveCan(request: AdminCanRequest) {
repository.save(request.toEntity())
}
@Transactional
fun deleteCan(id: Long) {
val can = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.")
can.status = CanStatus.END_OF_SALE
}
@Transactional
fun charge(request: AdminCanChargeRequest) {
val member = memberRepository.findByIdOrNull(request.memberId)
?: throw SodaException("잘못된 회원번호 입니다.")
if (request.can <= 0) throw SodaException("0 코인 이상 입력하세요.")
if (request.method.isBlank()) throw SodaException("기록내용을 입력하세요.")
val charge = Charge(0, request.can, status = ChargeStatus.ADMIN)
charge.title = "${request.can.moneyFormat()}"
charge.member = member
val payment = Payment(status = PaymentStatus.COMPLETE, paymentGateway = PaymentGateway.PG)
payment.method = request.method
charge.payment = payment
chargeRepository.save(charge)
member.pgRewardCan += charge.rewardCan
}
}

View File

@ -0,0 +1,26 @@
package kr.co.vividnext.sodalive.admin.charge
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/charge/status")
class AdminChargeStatusController(private val service: AdminChargeStatusService) {
@GetMapping
fun getChargeStatus(
@RequestParam startDateStr: String,
@RequestParam endDateStr: String
) = ApiResponse.ok(service.getChargeStatus(startDateStr, endDateStr))
@GetMapping("/detail")
fun getChargeStatusDetail(
@RequestParam startDateStr: String,
@RequestParam paymentGateway: PaymentGateway
) = ApiResponse.ok(service.getChargeStatusDetail(startDateStr, paymentGateway))
}

View File

@ -0,0 +1,90 @@
package kr.co.vividnext.sodalive.admin.charge
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.can.QCan.can1
import kr.co.vividnext.sodalive.can.charge.ChargeStatus
import kr.co.vividnext.sodalive.can.charge.QCharge.charge
import kr.co.vividnext.sodalive.can.payment.PaymentStatus
import kr.co.vividnext.sodalive.can.payment.QPayment.payment
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@Repository
class AdminChargeStatusQueryRepository(private val queryFactory: JPAQueryFactory) {
fun getChargeStatus(startDate: LocalDateTime, endDate: LocalDateTime): List<GetChargeStatusQueryDto> {
val formattedDate = Expressions.stringTemplate(
"DATE_FORMAT({0}, {1})",
Expressions.dateTimeTemplate(
LocalDateTime::class.java,
"CONVERT_TZ({0},{1},{2})",
charge.createdAt,
"UTC",
"Asia/Seoul"
),
"%Y-%m-%d"
)
return queryFactory
.select(
QGetChargeStatusQueryDto(
formattedDate,
payment.price.sum(),
can1.price.sum(),
payment.id.count(),
payment.paymentGateway
)
)
.from(payment)
.innerJoin(payment.charge, charge)
.leftJoin(charge.can, can1)
.where(
charge.createdAt.goe(startDate)
.and(charge.createdAt.loe(endDate))
.and(charge.status.eq(ChargeStatus.CHARGE))
.and(payment.status.eq(PaymentStatus.COMPLETE))
)
.groupBy(formattedDate, payment.paymentGateway)
.orderBy(formattedDate.desc())
.fetch()
}
fun getChargeStatusDetail(startDate: LocalDateTime, endDate: LocalDateTime): List<GetChargeStatusDetailQueryDto> {
val formattedDate = Expressions.stringTemplate(
"DATE_FORMAT({0}, {1})",
Expressions.dateTimeTemplate(
LocalDateTime::class.java,
"CONVERT_TZ({0},{1},{2})",
charge.createdAt,
"UTC",
"Asia/Seoul"
),
"%Y-%m-%d %H:%i:%s"
)
return queryFactory
.select(
QGetChargeStatusDetailQueryDto(
member.id,
member.nickname,
payment.method.coalesce(""),
payment.price,
can1.price,
formattedDate
)
)
.from(charge)
.innerJoin(charge.member, member)
.innerJoin(charge.payment, payment)
.leftJoin(charge.can, can1)
.where(
charge.createdAt.goe(startDate)
.and(charge.createdAt.loe(endDate))
.and(charge.status.eq(ChargeStatus.CHARGE))
.and(payment.status.eq(PaymentStatus.COMPLETE))
)
.orderBy(formattedDate.desc())
.fetch()
}
}

View File

@ -0,0 +1,101 @@
package kr.co.vividnext.sodalive.admin.charge
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
import org.springframework.stereotype.Service
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Service
class AdminChargeStatusService(val repository: AdminChargeStatusQueryRepository) {
fun getChargeStatus(startDateStr: String, endDateStr: String): List<GetChargeStatusResponse> {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val startDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(0, 0, 0)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
val endDate = LocalDate.parse(endDateStr, dateTimeFormatter).atTime(23, 59, 59)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
var totalChargeAmount = 0
var totalChargeCount = 0L
val chargeStatusList = repository.getChargeStatus(startDate, endDate)
.asSequence()
.map {
val chargeAmount = if (it.paymentGateWay == PaymentGateway.APPLE_IAP) {
it.appleChargeAmount.toInt()
} else {
it.pgChargeAmount
}
val chargeCount = it.chargeCount
totalChargeAmount += chargeAmount
totalChargeCount += chargeCount
GetChargeStatusResponse(
date = it.date,
chargeAmount = chargeAmount,
chargeCount = chargeCount,
pg = it.paymentGateWay.name
)
}
.toMutableList()
chargeStatusList.add(
0,
GetChargeStatusResponse(
date = "합계",
chargeAmount = totalChargeAmount,
chargeCount = totalChargeCount,
pg = ""
)
)
return chargeStatusList.toList()
}
fun getChargeStatusDetail(
startDateStr: String,
paymentGateway: PaymentGateway
): List<GetChargeStatusDetailResponse> {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val startDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(0, 0, 0)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
val endDate = LocalDate.parse(startDateStr, dateTimeFormatter).atTime(23, 59, 59)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
return repository.getChargeStatusDetail(startDate, endDate)
.asSequence()
.filter {
if (paymentGateway == PaymentGateway.APPLE_IAP) {
it.appleChargeAmount > 0
} else {
it.pgChargeAmount > 0
}
}
.map {
GetChargeStatusDetailResponse(
accountId = it.accountId,
nickname = it.nickname,
method = it.method,
amount = if (paymentGateway == PaymentGateway.APPLE_IAP) {
it.appleChargeAmount.toInt()
} else {
it.pgChargeAmount
},
datetime = it.datetime
)
}
.toList()
}
}

View File

@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.admin.charge
import com.querydsl.core.annotations.QueryProjection
data class GetChargeStatusDetailQueryDto @QueryProjection constructor(
val accountId: Long,
val nickname: String,
val method: String,
val appleChargeAmount: Double,
val pgChargeAmount: Int,
val datetime: String
)

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.admin.charge
data class GetChargeStatusDetailResponse(
val accountId: Long,
val nickname: String,
val method: String,
val amount: Int,
val datetime: String
)

View File

@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.admin.charge
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.can.payment.PaymentGateway
data class GetChargeStatusQueryDto @QueryProjection constructor(
val date: String,
val appleChargeAmount: Double,
val pgChargeAmount: Int,
val chargeCount: Long,
val paymentGateWay: PaymentGateway
)

View File

@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.admin.charge
data class GetChargeStatusResponse(
val date: String,
val chargeAmount: Int,
val chargeCount: Long,
val pg: String
)

View File

@ -0,0 +1,30 @@
package kr.co.vividnext.sodalive.admin.content
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@PreAuthorize("hasRole('ADMIN')")
@RequestMapping("/admin/audio-content")
class AdminContentController(private val service: AdminContentService) {
@GetMapping("/list")
fun getAudioContentList(pageable: Pageable) = ApiResponse.ok(service.getAudioContentList(pageable))
@GetMapping("/search")
fun searchAudioContent(
@RequestParam(value = "search_word") searchWord: String,
pageable: Pageable
) = ApiResponse.ok(service.searchAudioContent(searchWord, pageable))
@PutMapping
fun modifyAudioContent(
@RequestBody request: UpdateAdminContentRequest
) = ApiResponse.ok(service.updateAudioContent(request))
}

View File

@ -0,0 +1,119 @@
package kr.co.vividnext.sodalive.admin.content
import com.querydsl.core.types.dsl.DateTimePath
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.core.types.dsl.StringTemplate
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.AudioContent
import kr.co.vividnext.sodalive.content.QAudioContent.audioContent
import kr.co.vividnext.sodalive.content.hashtag.QAudioContentHashTag.audioContentHashTag
import kr.co.vividnext.sodalive.content.hashtag.QHashTag.hashTag
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration
import kr.co.vividnext.sodalive.content.theme.QAudioContentTheme.audioContentTheme
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
import java.time.LocalDateTime
@Repository
interface AdminContentRepository : JpaRepository<AudioContent, Long>, AdminAudioContentQueryRepository
interface AdminAudioContentQueryRepository {
fun getAudioContentTotalCount(searchWord: String = ""): Int
fun getAudioContentList(offset: Long, limit: Long, searchWord: String = ""): List<GetAdminContentListItem>
fun getHashTagList(audioContentId: Long): List<String>
}
class AdminAudioContentQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory
) : AdminAudioContentQueryRepository {
override fun getAudioContentTotalCount(searchWord: String): Int {
var where = audioContent.duration.isNotNull
.and(audioContent.member.isNotNull)
.and(audioContent.isActive.isTrue)
if (searchWord.trim().length > 1) {
where = where.and(
audioContent.title.contains(searchWord)
.or(audioContent.member.nickname.contains(searchWord))
)
}
return queryFactory
.select(audioContent.id)
.from(audioContent)
.where(where)
.fetch()
.size
}
override fun getAudioContentList(offset: Long, limit: Long, searchWord: String): List<GetAdminContentListItem> {
var where = audioContent.duration.isNotNull
.and(audioContent.member.isNotNull)
.and(audioContent.isActive.isTrue)
if (searchWord.trim().length > 1) {
where = where.and(
audioContent.title.contains(searchWord)
.or(audioContent.member.nickname.contains(searchWord))
)
}
return queryFactory
.select(
QGetAdminContentListItem(
audioContent.id,
audioContent.title,
audioContent.detail,
audioContentCuration.title,
audioContentCuration.id.nullif(0),
audioContent.coverImage,
audioContent.member!!.nickname,
audioContentTheme.theme,
audioContent.price,
audioContent.isAdult,
audioContent.duration,
audioContent.content,
formattedDateExpression(audioContent.createdAt)
)
)
.from(audioContent)
.leftJoin(audioContent.curation, audioContentCuration)
.innerJoin(audioContent.theme, audioContentTheme)
.where(where)
.offset(offset)
.limit(limit)
.orderBy(audioContent.id.desc())
.fetch()
}
override fun getHashTagList(audioContentId: Long): List<String> {
return queryFactory
.select(hashTag.tag)
.from(audioContentHashTag)
.innerJoin(audioContentHashTag.hashTag, hashTag)
.innerJoin(audioContentHashTag.audioContent, audioContent)
.where(
audioContent.duration.isNotNull
.and(audioContent.member.isNotNull)
.and(audioContentHashTag.audioContent.id.eq(audioContentId))
)
.fetch()
}
private fun formattedDateExpression(
dateTime: DateTimePath<LocalDateTime>,
format: String = "%Y-%m-%d"
): StringTemplate {
return Expressions.stringTemplate(
"DATE_FORMAT({0}, {1})",
Expressions.dateTimeTemplate(
LocalDateTime::class.java,
"CONVERT_TZ({0},{1},{2})",
dateTime,
"UTC",
"Asia/Seoul"
),
format
)
}
}

View File

@ -0,0 +1,121 @@
package kr.co.vividnext.sodalive.admin.content
import kr.co.vividnext.sodalive.admin.content.curation.AdminContentCurationRepository
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.Pageable
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class AdminContentService(
private val repository: AdminContentRepository,
private val audioContentCloudFront: AudioContentCloudFront,
private val curationRepository: AdminContentCurationRepository,
@Value("\${cloud.aws.cloud-front.host}")
private val coverImageHost: String
) {
fun getAudioContentList(pageable: Pageable): GetAdminContentListResponse {
val totalCount = repository.getAudioContentTotalCount()
val audioContentAndThemeList = repository.getAudioContentList(
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
val audioContentList = audioContentAndThemeList
.asSequence()
.map {
val tags = repository
.getHashTagList(audioContentId = it.audioContentId)
.joinToString(" ") { tag -> tag }
it.tags = tags
it
}
.map {
it.contentUrl = audioContentCloudFront.generateSignedURL(
resourcePath = it.contentUrl,
expirationTime = 1000 * 60 * 60 * (it.remainingTime.split(":")[0].toLong() + 2)
)
it
}
.map {
it.coverImageUrl = "$coverImageHost/${it.coverImageUrl}"
it
}
.toList()
return GetAdminContentListResponse(totalCount, audioContentList)
}
fun searchAudioContent(searchWord: String, pageable: Pageable): GetAdminContentListResponse {
if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.")
val totalCount = repository.getAudioContentTotalCount(searchWord)
val audioContentAndThemeList = repository.getAudioContentList(
offset = pageable.offset,
limit = pageable.pageSize.toLong(),
searchWord = searchWord
)
val audioContentList = audioContentAndThemeList
.asSequence()
.map {
val tags = repository
.getHashTagList(audioContentId = it.audioContentId)
.joinToString(" ") { tag -> tag }
it.tags = tags
it
}
.map {
it.contentUrl = audioContentCloudFront.generateSignedURL(
resourcePath = it.contentUrl,
expirationTime = 1000 * 60 * 60 * (it.remainingTime.split(":")[0].toLong() + 2)
)
it
}
.map {
it.coverImageUrl = "$coverImageHost/${it.coverImageUrl}"
it
}
.toList()
return GetAdminContentListResponse(totalCount, audioContentList)
}
@Transactional
fun updateAudioContent(request: UpdateAdminContentRequest) {
val audioContent = repository.findByIdOrNull(id = request.id)
?: throw SodaException("없는 콘텐츠 입니다.")
if (request.isDefaultCoverImage) {
audioContent.coverImage = "profile/default_profile.png"
}
if (request.isActive != null) {
audioContent.isActive = request.isActive
}
if (request.isAdult != null) {
audioContent.isAdult = request.isAdult
}
if (request.isCommentAvailable != null) {
audioContent.isCommentAvailable = request.isCommentAvailable
}
if (request.title != null) {
audioContent.title = request.title
}
if (request.detail != null) {
audioContent.detail = request.detail
}
if (request.curationId != null) {
val curation = curationRepository.findByIdAndActive(id = request.curationId)
audioContent.curation = curation
}
}
}

View File

@ -0,0 +1,26 @@
package kr.co.vividnext.sodalive.admin.content
import com.querydsl.core.annotations.QueryProjection
data class GetAdminContentListResponse(
val totalCount: Int,
val items: List<GetAdminContentListItem>
)
data class GetAdminContentListItem @QueryProjection constructor(
val audioContentId: Long,
val title: String,
val detail: String,
val curationTitle: String?,
val curationId: Long,
var coverImageUrl: String,
val creatorNickname: String,
val theme: String,
val price: Int,
val isAdult: Boolean,
val remainingTime: String,
var contentUrl: String,
val date: String
) {
var tags: String = ""
}

View File

@ -0,0 +1,12 @@
package kr.co.vividnext.sodalive.admin.content
data class UpdateAdminContentRequest(
val id: Long,
val isDefaultCoverImage: Boolean,
val title: String?,
val detail: String?,
val curationId: Long?,
val isAdult: Boolean?,
val isActive: Boolean?,
val isCommentAvailable: Boolean?
)

View File

@ -0,0 +1,37 @@
package kr.co.vividnext.sodalive.admin.content.banner
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@RequestMapping("/admin/audio-content/banner")
@PreAuthorize("hasRole('ADMIN')")
class AdminContentBannerController(private val service: AdminContentBannerService) {
@PostMapping
fun createAudioContentMainBanner(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.createAudioContentMainBanner(image, requestString))
@PutMapping
fun modifyAudioContentMainBanner(
@RequestPart("image", required = false) image: MultipartFile? = null,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.updateAudioContentMainBanner(image, requestString))
@PutMapping("/orders")
fun updateBannerOrders(
@RequestBody request: UpdateBannerOrdersRequest
) = ApiResponse.ok(service.updateBannerOrders(request.ids), "수정되었습니다.")
@GetMapping
fun getAudioContentMainBannerList() = ApiResponse.ok(service.getAudioContentMainBannerList())
}

View File

@ -0,0 +1,46 @@
package kr.co.vividnext.sodalive.admin.content.banner
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner
import kr.co.vividnext.sodalive.content.main.banner.QAudioContentBanner.audioContentBanner
import kr.co.vividnext.sodalive.event.QEvent.event
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface AdminContentBannerRepository : JpaRepository<AudioContentBanner, Long>, AdminContentBannerQueryRepository
interface AdminContentBannerQueryRepository {
fun getAudioContentMainBannerList(): List<GetAdminContentBannerResponse>
}
class AdminContentBannerQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) : AdminContentBannerQueryRepository {
override fun getAudioContentMainBannerList(): List<GetAdminContentBannerResponse> {
return queryFactory
.select(
QGetAdminContentBannerResponse(
audioContentBanner.id,
audioContentBanner.type,
audioContentBanner.thumbnailImage.prepend("/").prepend(cloudFrontHost),
audioContentBanner.event.id,
audioContentBanner.event.thumbnailImage,
audioContentBanner.creator.id,
audioContentBanner.creator.nickname,
audioContentBanner.link,
audioContentBanner.isAdult
)
)
.from(audioContentBanner)
.leftJoin(audioContentBanner.event, event)
.leftJoin(audioContentBanner.creator, member)
.where(audioContentBanner.isActive.isTrue)
.orderBy(audioContentBanner.orders.asc())
.fetch()
}
}

View File

@ -0,0 +1,144 @@
package kr.co.vividnext.sodalive.admin.content.banner
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBanner
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
import kr.co.vividnext.sodalive.event.EventRepository
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
class AdminContentBannerService(
private val s3Uploader: S3Uploader,
private val repository: AdminContentBannerRepository,
private val memberRepository: MemberRepository,
private val eventRepository: EventRepository,
private val objectMapper: ObjectMapper,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String
) {
@Transactional
fun createAudioContentMainBanner(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateContentBannerRequest::class.java)
if (request.type == AudioContentBannerType.CREATOR && request.creatorId == null) {
throw SodaException("크리에이터를 선택하세요.")
}
if (request.type == AudioContentBannerType.LINK && request.link == null) {
throw SodaException("링크 url을 입력하세요.")
}
if (request.type == AudioContentBannerType.EVENT && request.eventId == null) {
throw SodaException("이벤트를 선택하세요.")
}
val event = if (request.eventId != null && request.eventId > 0) {
eventRepository.findByIdOrNull(request.eventId)
} else {
null
}
val creator = if (request.creatorId != null && request.creatorId > 0) {
memberRepository.findByIdOrNull(request.creatorId)
} else {
null
}
val audioContentBanner = AudioContentBanner(type = request.type)
audioContentBanner.link = request.link
audioContentBanner.isAdult = request.isAdult
audioContentBanner.event = event
audioContentBanner.creator = creator
repository.save(audioContentBanner)
val fileName = generateFileName()
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audio_content_banner/${audioContentBanner.id}/$fileName"
)
audioContentBanner.thumbnailImage = imagePath
}
@Transactional
fun updateAudioContentMainBanner(image: MultipartFile?, requestString: String) {
val request = objectMapper.readValue(requestString, UpdateContentBannerRequest::class.java)
val audioContentBanner = repository.findByIdOrNull(request.id)
?: throw SodaException("잘못된 요청입니다.")
if (image != null) {
val fileName = generateFileName()
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "audio_content_banner/${audioContentBanner.id}/$fileName"
)
audioContentBanner.thumbnailImage = imagePath
}
if (request.isAdult != null) {
audioContentBanner.isAdult = request.isAdult
}
if (request.isActive != null) {
audioContentBanner.isActive = request.isActive
}
if (request.type != null) {
audioContentBanner.creator = null
audioContentBanner.event = null
audioContentBanner.link = null
if (request.type == AudioContentBannerType.CREATOR) {
if (request.creatorId != null) {
val creator = memberRepository.findByIdOrNull(request.creatorId)
?: throw SodaException("크리에이터를 선택하세요.")
audioContentBanner.creator = creator
} else {
throw SodaException("크리에이터를 선택하세요.")
}
} else if (request.type == AudioContentBannerType.LINK) {
if (request.link != null) {
audioContentBanner.link = request.link
} else {
throw SodaException("링크 url을 입력하세요.")
}
} else if (request.type == AudioContentBannerType.EVENT) {
if (request.eventId != null) {
val event = eventRepository.findByIdOrNull(request.eventId)
?: throw SodaException("이벤트를 선택하세요.")
audioContentBanner.event = event
} else {
throw SodaException("이벤트를 선택하세요.")
}
}
audioContentBanner.type = request.type
}
}
@Transactional
fun updateBannerOrders(ids: List<Long>) {
for (index in ids.indices) {
val tag = repository.findByIdOrNull(ids[index])
if (tag != null) {
tag.orders = index + 1
}
}
}
fun getAudioContentMainBannerList(): List<GetAdminContentBannerResponse> {
return repository.getAudioContentMainBannerList()
}
}

View File

@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.admin.content.banner
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
data class CreateContentBannerRequest(
val type: AudioContentBannerType,
val eventId: Long?,
val creatorId: Long?,
val link: String?,
val isAdult: Boolean
)

View File

@ -0,0 +1,16 @@
package kr.co.vividnext.sodalive.admin.content.banner
import com.querydsl.core.annotations.QueryProjection
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
data class GetAdminContentBannerResponse @QueryProjection constructor(
val id: Long,
val type: AudioContentBannerType,
val thumbnailImageUrl: String,
val eventId: Long?,
val eventThumbnailImage: String?,
val creatorId: Long?,
val creatorNickname: String?,
val link: String?,
val isAdult: Boolean
)

View File

@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.admin.content.banner
data class UpdateBannerOrdersRequest(
val ids: List<Long>
)

View File

@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.admin.content.banner
import kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerType
data class UpdateContentBannerRequest(
val id: Long,
val type: AudioContentBannerType?,
val eventId: Long?,
val creatorId: Long?,
val link: String?,
val isAdult: Boolean?,
val isActive: Boolean?
)

View File

@ -0,0 +1,33 @@
package kr.co.vividnext.sodalive.admin.content.curation
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/admin/audio-content/curation")
@PreAuthorize("hasRole('ADMIN')")
class AdminContentCurationController(private val service: AdminContentCurationService) {
@PostMapping
fun createContentCuration(
@RequestBody request: CreateContentCurationRequest
) = ApiResponse.ok(service.createContentCuration(request))
@PutMapping
fun updateContentCuration(
@RequestBody request: UpdateContentCurationRequest
) = ApiResponse.ok(service.updateContentCuration(request))
@PutMapping("/orders")
fun updateContentCurationOrders(
@RequestBody request: UpdateContentCurationOrdersRequest
) = ApiResponse.ok(service.updateContentCurationOrders(request.ids), "수정되었습니다.")
@GetMapping
fun getContentCurationList() = ApiResponse.ok(service.getContentCurationList())
}

View File

@ -0,0 +1,48 @@
package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
import kr.co.vividnext.sodalive.content.main.curation.QAudioContentCuration.audioContentCuration
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface AdminContentCurationRepository :
JpaRepository<AudioContentCuration, Long>,
AdminContentCurationQueryRepository
interface AdminContentCurationQueryRepository {
fun getAudioContentCurationList(): List<GetAdminContentCurationResponse>
fun findByIdAndActive(id: Long): AudioContentCuration?
}
@Repository
class AdminContentCurationQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory
) : AdminContentCurationQueryRepository {
override fun getAudioContentCurationList(): List<GetAdminContentCurationResponse> {
return queryFactory
.select(
QGetAdminContentCurationResponse(
audioContentCuration.id,
audioContentCuration.title,
audioContentCuration.description,
audioContentCuration.isAdult
)
)
.from(audioContentCuration)
.where(audioContentCuration.isActive.isTrue)
.orderBy(audioContentCuration.orders.asc())
.fetch()
}
override fun findByIdAndActive(id: Long): AudioContentCuration? {
return queryFactory
.selectFrom(audioContentCuration)
.where(
audioContentCuration.id.eq(id)
.and(audioContentCuration.isActive.isTrue)
)
.fetchFirst()
}
}

View File

@ -0,0 +1,60 @@
package kr.co.vividnext.sodalive.admin.content.curation
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.content.main.curation.AudioContentCuration
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class AdminContentCurationService(
private val repository: AdminContentCurationRepository
) {
@Transactional
fun createContentCuration(request: CreateContentCurationRequest) {
repository.save(
AudioContentCuration(
title = request.title,
description = request.description,
isAdult = request.isAdult
)
)
}
@Transactional
fun updateContentCuration(request: UpdateContentCurationRequest) {
val audioContentCuration = repository.findByIdOrNull(id = request.id)
?: throw SodaException("잘못된 요청입니다.")
if (request.title != null) {
audioContentCuration.title = request.title
}
if (request.description != null) {
audioContentCuration.description = request.description
}
if (request.isAdult != null) {
audioContentCuration.isAdult = request.isAdult
}
if (request.isActive != null) {
audioContentCuration.isActive = request.isActive
}
}
@Transactional
fun updateContentCurationOrders(ids: List<Long>) {
for (index in ids.indices) {
val audioContentCuration = repository.findByIdOrNull(ids[index])
if (audioContentCuration != null) {
audioContentCuration.orders = index + 1
}
}
}
fun getContentCurationList(): List<GetAdminContentCurationResponse> {
return repository.getAudioContentCurationList()
}
}

View File

@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.content.curation
data class CreateContentCurationRequest(
val title: String,
val description: String,
val isAdult: Boolean
)
data class UpdateContentCurationRequest(
val id: Long,
val title: String?,
val description: String?,
val isAdult: Boolean?,
val isActive: Boolean?
)
data class UpdateContentCurationOrdersRequest(
val ids: List<Long>
)

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.admin.content.curation
import com.querydsl.core.annotations.QueryProjection
data class GetAdminContentCurationResponse @QueryProjection constructor(
val id: Long,
val title: String,
val description: String,
val isAdult: Boolean
)

View File

@ -0,0 +1,32 @@
package kr.co.vividnext.sodalive.admin.event
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/event/charge")
class AdminChargeEventController(private val service: AdminChargeEventService) {
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
fun createChargeEvent(@RequestBody request: CreateChargeEventRequest): ApiResponse<Any> {
service.createChargeEvent(request)
return ApiResponse.ok(null, "등록되었습니다.")
}
@PutMapping
@PreAuthorize("hasRole('ADMIN')")
fun modifyChargeEvent(@RequestBody request: ModifyChargeEventRequest) = ApiResponse.ok(
service.modifyChargeEvent(request),
"수정되었습니다."
)
@GetMapping("/list")
@PreAuthorize("hasRole('ADMIN')")
fun getChargeEventList() = ApiResponse.ok(service.getChargeEventList())
}

View File

@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.admin.event
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.admin.event.QChargeEvent.chargeEvent
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface AdminChargeEventRepository : JpaRepository<ChargeEvent, Long>, AdminChargeEventQueryRepository
interface AdminChargeEventQueryRepository {
fun getChargeEventList(): List<ChargeEvent>
}
class AdminChargeEventQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminChargeEventQueryRepository {
override fun getChargeEventList(): List<ChargeEvent> {
return queryFactory
.selectFrom(chargeEvent)
.orderBy(chargeEvent.createdAt.desc())
.fetch()
}
}

View File

@ -0,0 +1,100 @@
package kr.co.vividnext.sodalive.admin.event
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.time.LocalDate
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Service
@Transactional(readOnly = true)
class AdminChargeEventService(private val repository: AdminChargeEventRepository) {
@Transactional
fun createChargeEvent(request: CreateChargeEventRequest): Long {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val startDate = LocalDate.parse(request.startDateString, dateTimeFormatter).atTime(0, 0)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
val endDate = LocalDate.parse(request.endDateString, dateTimeFormatter).atTime(23, 59, 59)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
val chargeEvent = ChargeEvent(
title = request.title,
startDate = startDate,
endDate = endDate,
availableCount = request.availableCount,
addPercent = request.addPercent / 100f
)
return repository.save(chargeEvent).id!!
}
@Transactional
fun modifyChargeEvent(request: ModifyChargeEventRequest) {
val chargeEvent = repository.findByIdOrNull(request.id)
?: throw SodaException("해당하는 충전이벤트가 없습니다\n다시 시도해 주세요.")
if (request.title != null) {
chargeEvent.title = request.title
}
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
if (request.startDateString != null) {
chargeEvent.startDate = LocalDate.parse(request.startDateString, dateTimeFormatter).atTime(0, 0)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
}
if (request.endDateString != null) {
chargeEvent.endDate = LocalDate.parse(request.endDateString, dateTimeFormatter).atTime(23, 59, 59)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
}
if (request.availableCount != null) {
chargeEvent.availableCount = request.availableCount
}
if (request.addPercent != null) {
chargeEvent.addPercent = request.addPercent / 100f
}
if (request.isActive != null) {
chargeEvent.isActive = request.isActive
}
}
fun getChargeEventList(): List<GetChargeEventListResponse> {
return repository.getChargeEventList()
.map {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd")
val startDate = it.startDate
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.format(dateTimeFormatter)
val endDate = it.endDate
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.format(dateTimeFormatter)
GetChargeEventListResponse(
id = it.id!!,
title = it.title,
startDate = startDate,
endDate = endDate,
availableCount = it.availableCount,
addPercent = (it.addPercent * 100).toInt(),
isActive = it.isActive
)
}
}
}

View File

@ -0,0 +1,15 @@
package kr.co.vividnext.sodalive.admin.event
import kr.co.vividnext.sodalive.common.BaseEntity
import java.time.LocalDateTime
import javax.persistence.Entity
@Entity
data class ChargeEvent(
var title: String,
var startDate: LocalDateTime,
var endDate: LocalDateTime,
var availableCount: Int,
var addPercent: Float,
var isActive: Boolean = true
) : BaseEntity()

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.admin.event
data class CreateChargeEventRequest(
val title: String,
val startDateString: String,
val endDateString: String,
val availableCount: Int,
val addPercent: Int
)

View File

@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.admin.event
data class GetChargeEventListResponse(
val id: Long,
val title: String,
val startDate: String,
val endDate: String,
val availableCount: Int,
val addPercent: Int,
val isActive: Boolean
)

View File

@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.admin.event
data class ModifyChargeEventRequest(
val id: Long,
val title: String? = null,
val startDateString: String? = null,
val endDateString: String? = null,
val availableCount: Int? = null,
val addPercent: Int? = null,
val isActive: Boolean? = null
)

View File

@ -0,0 +1,58 @@
package kr.co.vividnext.sodalive.admin.live
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@RequestMapping("/admin/live")
class AdminLiveController(private val service: AdminLiveService) {
@GetMapping
@PreAuthorize("hasRole('ADMIN')")
fun getOnAirLive() = ApiResponse.ok(data = service.getLiveList())
@GetMapping("/recommend-creator")
@PreAuthorize("hasRole('ADMIN')")
fun getRecommendCreatorBanner(pageable: Pageable) = ApiResponse.ok(service.getRecommendCreator(pageable))
@PostMapping("/recommend-creator")
fun createRecommendCreatorBanner(
@RequestParam("image") image: MultipartFile,
@RequestParam("creator_id") creatorId: Long,
@RequestParam("start_date") startDate: String,
@RequestParam("end_date") endDate: String,
@RequestParam("is_adult") isAdult: Boolean
) = ApiResponse.ok(
service.createRecommendCreatorBanner(image, creatorId, startDate, endDate, isAdult),
"등록되었습니다."
)
@PutMapping("/recommend-creator")
fun updateRecommendCreatorBanner(
@RequestParam("recommend_creator_banner_id") recommendCreatorBannerId: Long,
@RequestParam("image", required = false) image: MultipartFile?,
@RequestParam("creator_id", required = false) creatorId: Long?,
@RequestParam("start_date", required = false) startDate: String?,
@RequestParam("end_date", required = false) endDate: String?,
@RequestParam("is_adult", required = false) isAdult: Boolean?
) = ApiResponse.ok(
service.updateRecommendCreatorBanner(recommendCreatorBannerId, image, creatorId, startDate, endDate, isAdult),
"수정되었습니다."
)
@PutMapping("/recommend-creator/orders")
fun updateRecommendCreatorBannerOrders(
@RequestBody request: UpdateAdminRecommendCreatorBannerOrdersRequest
) = ApiResponse.ok(
service.updateRecommendCreatorBannerOrders(request.firstOrders, request.ids),
"수정되었습니다."
)
}

View File

@ -0,0 +1,39 @@
package kr.co.vividnext.sodalive.admin.live
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.live.recommend.QRecommendLiveCreatorBanner.recommendLiveCreatorBanner
import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBanner
import kr.co.vividnext.sodalive.live.room.LiveRoom
import kr.co.vividnext.sodalive.live.room.QLiveRoom.liveRoom
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Repository
@Repository
class AdminLiveRoomQueryRepository(private val queryFactory: JPAQueryFactory) {
fun getLiveRoomList(): List<LiveRoom> {
return queryFactory
.selectFrom(liveRoom)
.innerJoin(liveRoom.member, member)
.where(liveRoom.isActive.isTrue)
.orderBy(liveRoom.channelName.desc(), liveRoom.beginDateTime.asc())
.fetch()
}
fun getRecommendCreatorTotalCount(): Int {
return queryFactory
.select(recommendLiveCreatorBanner.id)
.from(recommendLiveCreatorBanner)
.fetch()
.size
}
fun getRecommendCreatorList(pageable: Pageable): List<RecommendLiveCreatorBanner> {
return queryFactory
.selectFrom(recommendLiveCreatorBanner)
.offset(pageable.offset)
.limit(pageable.pageSize.toLong())
.orderBy(recommendLiveCreatorBanner.orders.asc(), recommendLiveCreatorBanner.id.desc())
.fetch()
}
}

View File

@ -0,0 +1,240 @@
package kr.co.vividnext.sodalive.admin.live
import com.amazonaws.services.s3.model.ObjectMetadata
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBanner
import kr.co.vividnext.sodalive.live.recommend.RecommendLiveCreatorBannerRepository
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.Pageable
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
import java.time.LocalDateTime
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Service
class AdminLiveService(
private val recommendCreatorBannerRepository: RecommendLiveCreatorBannerRepository,
private val repository: AdminLiveRoomQueryRepository,
private val memberRepository: MemberRepository,
private val s3Uploader: S3Uploader,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String,
@Value("\${cloud.aws.cloud-front.host}")
private val coverImageHost: String
) {
fun getLiveList(): GetLiveResponse {
return GetLiveResponse(
liveList = repository.getLiveRoomList()
.asSequence()
.map {
GetLiveResponseItem(
id = it.id!!,
title = it.title,
content = it.notice,
managerNickname = it.member!!.nickname,
coverImageUrl = if (it.coverImage!!.startsWith("https://")) {
it.coverImage!!
} else {
"$coverImageHost/${it.coverImage!!}"
},
channelName = it.channelName ?: "",
type = it.type,
password = it.password,
isAdult = it.isAdult
)
}
.toList()
)
}
fun getRecommendCreator(pageable: Pageable): GetAdminRecommendCreatorResponse {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
val totalCount = repository.getRecommendCreatorTotalCount()
val recommendCreatorList = repository
.getRecommendCreatorList(pageable)
.asSequence()
.map {
GetAdminRecommendCreatorResponseItem(
it.id!!,
"$coverImageHost/${it.image}",
it.creator!!.id!!,
it.creator!!.nickname,
it.startDate
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.toLocalDateTime()
.format(dateTimeFormatter),
it.endDate
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.toLocalDateTime()
.format(dateTimeFormatter),
it.isAdult
)
}
.toList()
return GetAdminRecommendCreatorResponse(
totalCount = totalCount,
recommendCreatorList = recommendCreatorList
)
}
@Transactional
fun createRecommendCreatorBanner(
image: MultipartFile,
creatorId: Long,
startDateString: String,
endDateString: String,
isAdult: Boolean
): Long {
if (creatorId < 1) throw SodaException("올바른 크리에이터를 선택해 주세요.")
val creator = memberRepository.findCreatorByIdOrNull(memberId = creatorId)
?: throw SodaException("올바른 크리에이터를 선택해 주세요.")
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
val startDate = LocalDateTime.parse(startDateString, dateTimeFormatter)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
val nowDate = LocalDateTime.now()
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
if (startDate < nowDate) throw SodaException("노출 시작일은 현재시간 이후로 설정하셔야 합니다.")
val endDate = LocalDateTime.parse(endDateString, dateTimeFormatter)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
if (endDate < nowDate) throw SodaException("노출 종료일은 현재시간 이후로 설정하셔야 합니다.")
if (endDate <= startDate) throw SodaException("노출 시작일은 노출 종료일 이전으로 설정하셔야 합니다.")
val recommendCreatorBanner = RecommendLiveCreatorBanner(
startDate = startDate,
endDate = endDate,
isAdult = isAdult
)
recommendCreatorBanner.creator = creator
recommendCreatorBannerRepository.save(recommendCreatorBanner)
val metadata = ObjectMetadata()
metadata.contentLength = image.size
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "recommend_creator_banner/${recommendCreatorBanner.id}/${generateFileName()}",
metadata = metadata
)
recommendCreatorBanner.image = imagePath
return recommendCreatorBanner.id!!
}
@Transactional
fun updateRecommendCreatorBanner(
recommendCreatorBannerId: Long,
image: MultipartFile?,
creatorId: Long?,
startDateString: String?,
endDateString: String?,
isAdult: Boolean?
) {
val recommendCreatorBanner = recommendCreatorBannerRepository.findByIdOrNull(recommendCreatorBannerId)
?: throw SodaException("해당하는 추천라이브가 없습니다. 다시 확인해 주세요.")
if (creatorId != null) {
if (creatorId < 1) throw SodaException("올바른 크리에이터를 선택해 주세요.")
val creator = memberRepository.findCreatorByIdOrNull(memberId = creatorId)
?: throw SodaException("올바른 크리에이터를 선택해 주세요.")
recommendCreatorBanner.creator = creator
}
if (image != null) {
val metadata = ObjectMetadata()
metadata.contentLength = image.size
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "recommend_creator_banner/${recommendCreatorBanner.id}/${generateFileName()}",
metadata = metadata
)
recommendCreatorBanner.image = imagePath
}
if (startDateString != null) {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
val startDate = LocalDateTime.parse(startDateString, dateTimeFormatter)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
val endDate = if (endDateString != null) {
LocalDateTime.parse(endDateString, dateTimeFormatter)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
} else {
null
}
if (endDate != null) {
if (endDate <= startDate) {
throw SodaException("노출 시작일은 노출 종료일 이전으로 설정하셔야 합니다.")
}
recommendCreatorBanner.endDate = endDate
} else {
if (recommendCreatorBanner.endDate <= startDate) {
throw SodaException("노출 시작일은 노출 종료일 이전으로 설정하셔야 합니다.")
}
}
recommendCreatorBanner.startDate = startDate
} else if (endDateString != null) {
val dateTimeFormatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")
val endDate = LocalDateTime.parse(endDateString, dateTimeFormatter)
.atZone(ZoneId.of("Asia/Seoul"))
.withZoneSameInstant(ZoneId.of("UTC"))
.toLocalDateTime()
if (endDate <= recommendCreatorBanner.startDate) {
throw SodaException("노출 종료일은 노출 시작일 이후로 설정하셔야 합니다.")
}
recommendCreatorBanner.endDate = endDate
}
if (isAdult != null) {
recommendCreatorBanner.isAdult = isAdult
}
}
@Transactional
fun updateRecommendCreatorBannerOrders(firstOrders: Int, ids: List<Long>) {
for (index in ids.indices) {
val recommendCreatorBanner = recommendCreatorBannerRepository.findByIdOrNull(id = ids[index])
if (recommendCreatorBanner != null) {
recommendCreatorBanner.orders = firstOrders + index
}
}
}
}

View File

@ -0,0 +1,16 @@
package kr.co.vividnext.sodalive.admin.live
data class GetAdminRecommendCreatorResponse(
val totalCount: Int,
val recommendCreatorList: List<GetAdminRecommendCreatorResponseItem>
)
data class GetAdminRecommendCreatorResponseItem(
val id: Long,
val image: String,
val creatorId: Long,
val creatorNickname: String,
val startDate: String,
val endDate: String,
val isAdult: Boolean
)

View File

@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.live
import kr.co.vividnext.sodalive.live.room.LiveRoomType
data class GetLiveResponse(
val liveList: List<GetLiveResponseItem>
)
data class GetLiveResponseItem(
val id: Long,
val title: String,
val content: String,
val managerNickname: String,
val coverImageUrl: String,
val channelName: String,
val type: LiveRoomType,
val password: String?,
val isAdult: Boolean
)

View File

@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.admin.live
data class UpdateAdminRecommendCreatorBannerOrdersRequest(
val firstOrders: Int,
val ids: List<Long>
)

View File

@ -0,0 +1,35 @@
package kr.co.vividnext.sodalive.admin.member
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/admin/member")
@PreAuthorize("hasRole('ADMIN')")
class AdminMemberController(private val service: AdminMemberService) {
@GetMapping("/list")
fun getMemberList(pageable: Pageable) = ApiResponse.ok(service.getMemberList(pageable))
@GetMapping("/search")
fun searchMember(
@RequestParam(value = "search_word") searchWord: String,
pageable: Pageable
) = ApiResponse.ok(service.searchMember(searchWord, pageable))
@GetMapping("/creator/all/list")
fun getCreatorAllList() = ApiResponse.ok(service.getCreatorAllList())
@GetMapping("/creator/list")
fun getCreatorList(pageable: Pageable) = ApiResponse.ok(service.getCreatorList(pageable))
@GetMapping("/creator/search")
fun searchCreator(
@RequestParam(value = "search_word") searchWord: String,
pageable: Pageable
) = ApiResponse.ok(service.searchCreator(searchWord, pageable))
}

View File

@ -0,0 +1,112 @@
package kr.co.vividnext.sodalive.admin.member
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.data.jpa.repository.JpaRepository
interface AdminMemberRepository : JpaRepository<Member, Long>, AdminMemberQueryRepository
interface AdminMemberQueryRepository {
fun getMemberTotalCount(role: MemberRole? = null): Int
fun getMemberList(offset: Long, limit: Long, role: MemberRole? = null): List<Member>
fun searchMember(searchWord: String, offset: Long, limit: Long, role: MemberRole? = null): List<Member>
fun searchMemberTotalCount(searchWord: String, role: MemberRole? = null): Int
fun getCreatorAllList(): List<GetAdminCreatorAllListResponse>
}
class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminMemberQueryRepository {
override fun getMemberList(offset: Long, limit: Long, role: MemberRole?): List<Member> {
return queryFactory
.selectFrom(member)
.where(
member.role.ne(MemberRole.ADMIN)
.and(
if (role != null) {
member.role.eq(role)
} else {
null
}
)
)
.offset(offset)
.limit(limit)
.orderBy(member.id.desc())
.fetch()
}
override fun getMemberTotalCount(role: MemberRole?): Int {
return queryFactory
.select(member.id)
.from(member)
.where(
member.role.ne(MemberRole.ADMIN)
.and(
if (role != null) {
member.role.eq(role)
} else {
null
}
)
)
.fetch()
.size
}
override fun searchMember(searchWord: String, offset: Long, limit: Long, role: MemberRole?): List<Member> {
return queryFactory
.selectFrom(member)
.where(
member.nickname.contains(searchWord)
.or(member.email.contains(searchWord))
.and(
if (role != null) {
member.role.eq(role)
} else {
null
}
)
)
.offset(offset)
.limit(limit)
.orderBy(member.id.desc())
.fetch()
}
override fun searchMemberTotalCount(searchWord: String, role: MemberRole?): Int {
return queryFactory
.select(member.id)
.from(member)
.where(
member.nickname.contains(searchWord)
.or(member.email.contains(searchWord))
.and(
if (role != null) {
member.role.eq(role)
} else {
null
}
)
)
.fetch()
.size
}
override fun getCreatorAllList(): List<GetAdminCreatorAllListResponse> {
return queryFactory
.select(
QGetAdminCreatorAllListResponse(
member.id,
member.nickname
)
)
.from(member)
.where(
member.role.eq(MemberRole.CREATOR)
.and(member.isActive.isTrue)
)
.fetch()
}
}

View File

@ -0,0 +1,124 @@
package kr.co.vividnext.sodalive.admin.member
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.MemberRole
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.domain.Pageable
import org.springframework.stereotype.Service
import java.time.ZoneId
import java.time.format.DateTimeFormatter
@Service
class AdminMemberService(
private val repository: AdminMemberRepository,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
fun getMemberList(pageable: Pageable): GetAdminMemberListResponse {
val totalCount = repository.getMemberTotalCount()
val memberList = processMemberListToGetAdminMemberListResponseItemList(
memberList = repository.getMemberList(
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
return GetAdminMemberListResponse(totalCount, memberList)
}
fun searchMember(searchWord: String, pageable: Pageable): GetAdminMemberListResponse {
if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.")
val totalCount = repository.searchMemberTotalCount(searchWord = searchWord)
val memberList = processMemberListToGetAdminMemberListResponseItemList(
memberList = repository.searchMember(
searchWord = searchWord,
offset = pageable.offset,
limit = pageable.pageSize.toLong()
)
)
return GetAdminMemberListResponse(totalCount, memberList)
}
fun getCreatorList(pageable: Pageable): GetAdminMemberListResponse {
val totalCount = repository.getMemberTotalCount(role = MemberRole.CREATOR)
val creatorList = processMemberListToGetAdminMemberListResponseItemList(
memberList = repository.getMemberList(
offset = pageable.offset,
limit = pageable.pageSize.toLong(),
role = MemberRole.CREATOR
)
)
return GetAdminMemberListResponse(totalCount, creatorList)
}
fun searchCreator(searchWord: String, pageable: Pageable): GetAdminMemberListResponse {
if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.")
val totalCount = repository.searchMemberTotalCount(searchWord = searchWord, role = MemberRole.CREATOR)
val creatorList = processMemberListToGetAdminMemberListResponseItemList(
memberList = repository.searchMember(
searchWord = searchWord,
offset = pageable.offset,
limit = pageable.pageSize.toLong(),
role = MemberRole.CREATOR
)
)
return GetAdminMemberListResponse(totalCount, creatorList)
}
private fun processMemberListToGetAdminMemberListResponseItemList(
memberList: List<Member>
): List<GetAdminMemberListResponseItem> {
return memberList
.asSequence()
.map {
val userType = when (it.role) {
MemberRole.ADMIN -> "관리자"
MemberRole.USER -> "일반회원"
MemberRole.CREATOR -> "크리에이터"
MemberRole.AGENT -> "에이전트"
MemberRole.BOT -> ""
}
val signUpDate = it.createdAt!!
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
val signOutDate = if (it.signOutReasons.isNotEmpty()) {
it.signOutReasons.last().createdAt!!
.atZone(ZoneId.of("UTC"))
.withZoneSameInstant(ZoneId.of("Asia/Seoul"))
.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"))
} else {
""
}
GetAdminMemberListResponseItem(
id = it.id!!,
email = it.email,
nickname = it.nickname,
profileUrl = if (it.profileImage != null) {
"$cloudFrontHost/${it.profileImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
},
userType = userType,
container = it.container,
auth = it.auth != null,
signUpDate = signUpDate,
signOutDate = signOutDate,
isActive = it.isActive
)
}
.toList()
}
fun getCreatorAllList(): List<GetAdminCreatorAllListResponse> {
return repository.getCreatorAllList()
}
}

View File

@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.admin.member
import com.querydsl.core.annotations.QueryProjection
data class GetAdminCreatorAllListResponse @QueryProjection constructor(
val id: Long,
val nickname: String
)

View File

@ -0,0 +1,19 @@
package kr.co.vividnext.sodalive.admin.member
data class GetAdminMemberListResponse(
val totalCount: Int,
val items: List<GetAdminMemberListResponseItem>
)
data class GetAdminMemberListResponseItem(
val id: Long,
val email: String,
val nickname: String,
val profileUrl: String,
val userType: String,
val container: String,
val auth: Boolean,
val signUpDate: String,
val signOutDate: String,
val isActive: Boolean
)

View File

@ -0,0 +1,39 @@
package kr.co.vividnext.sodalive.admin.member.tag
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@RequestMapping("/admin/member/tag")
@PreAuthorize("hasRole('ADMIN')")
class AdminMemberTagController(private val service: AdminMemberTagService) {
@PostMapping
fun enrollmentCreatorTag(
@RequestPart("image") image: MultipartFile,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.uploadTagImage(image, requestString), "등록되었습니다.")
@DeleteMapping("/{id}")
fun deleteCreatorTag(@PathVariable id: Long) = ApiResponse.ok(service.deleteTag(id), "삭제되었습니다.")
@PutMapping("/{id}")
fun modifyCreatorTag(
@PathVariable id: Long,
@RequestPart("image") image: MultipartFile?,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.modifyTag(id, image, requestString), "수정되었습니다.")
@PutMapping("/orders")
fun updateTagOrders(
@RequestBody request: UpdateTagOrdersRequest
) = ApiResponse.ok(service.updateTagOrders(request.ids), "수정되었습니다.")
}

View File

@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.admin.member.tag
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.member.tag.CreatorTag
import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag
import org.springframework.data.jpa.repository.JpaRepository
interface AdminMemberTagRepository : JpaRepository<CreatorTag, Long>, AdminMemberTagQueryRepository
interface AdminMemberTagQueryRepository {
fun findByTag(tag: String): Long?
}
class AdminMemberTagQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminMemberTagQueryRepository {
override fun findByTag(tag: String): Long? {
return queryFactory
.select(creatorTag.id)
.from(creatorTag)
.where(creatorTag.tag.eq(tag))
.fetchFirst()
}
}

View File

@ -0,0 +1,83 @@
package kr.co.vividnext.sodalive.admin.member.tag
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.tag.CreatorTag
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
class AdminMemberTagService(
private val repository: AdminMemberTagRepository,
private val s3Uploader: S3Uploader,
private val objectMapper: ObjectMapper,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String
) {
@Transactional
fun uploadTagImage(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateMemberTagRequest::class.java)
tagExistCheck(request)
val fileName = generateFileName()
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "creator_tag/$fileName"
)
return createTag(request.tag, imagePath)
}
private fun tagExistCheck(request: CreateMemberTagRequest) {
repository.findByTag(request.tag)?.let { throw SodaException("이미 등록된 태그 입니다.") }
}
private fun createTag(tag: String, imagePath: String) {
repository.save(CreatorTag(tag, imagePath))
}
@Transactional
fun deleteTag(id: Long) {
val creatorTag = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.")
creatorTag.tag = "${creatorTag.tag}_deleted"
creatorTag.isActive = false
}
@Transactional
fun modifyTag(id: Long, image: MultipartFile?, requestString: String) {
val creatorTag = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.")
val request = objectMapper.readValue(requestString, CreateMemberTagRequest::class.java)
creatorTag.tag = request.tag
if (image != null) {
val fileName = generateFileName()
val imagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = bucket,
filePath = "creator_tag/$fileName"
)
creatorTag.image = imagePath
}
}
@Transactional
fun updateTagOrders(ids: List<Long>) {
for (index in ids.indices) {
val tag = repository.findByIdOrNull(ids[index])
if (tag != null) {
tag.orders = index + 1
}
}
}
}

View File

@ -0,0 +1,3 @@
package kr.co.vividnext.sodalive.admin.member.tag
data class CreateMemberTagRequest(val tag: String)

View File

@ -0,0 +1,3 @@
package kr.co.vividnext.sodalive.admin.member.tag
data class UpdateTagOrdersRequest(val ids: List<Long>)

View File

@ -1,13 +1,56 @@
package kr.co.vividnext.sodalive.event
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@RequestMapping("/event")
class EventController(private val service: EventService) {
@GetMapping
fun getEventList() = ApiResponse.ok(service.getEventList())
@GetMapping("/popup")
fun getEventPopup() = ApiResponse.ok(service.getEventPopup())
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
fun createEvent(
@RequestParam("thumbnail") thumbnail: MultipartFile,
@RequestParam(value = "detail", required = false) detail: MultipartFile? = null,
@RequestParam(value = "popup", required = false) popup: MultipartFile? = null,
@RequestParam(value = "link", required = false) link: String? = null,
@RequestParam(value = "title", required = false) title: String? = null,
@RequestParam(value = "isPopup") isPopup: Boolean
) = ApiResponse.ok(
service.save(thumbnail, detail, popup, link, title, isPopup),
"등록되었습니다."
)
@PutMapping
@PreAuthorize("hasRole('ADMIN')")
fun updateEvent(
@RequestParam(value = "id") id: Long,
@RequestParam(value = "thumbnail", required = false) thumbnail: MultipartFile? = null,
@RequestParam(value = "detail", required = false) detail: MultipartFile? = null,
@RequestParam(value = "popup", required = false) popup: MultipartFile? = null,
@RequestParam(value = "link", required = false) link: String? = null,
@RequestParam(value = "title", required = false) title: String? = null,
@RequestParam(value = "isPopup", required = false) isPopup: Boolean? = null
) = ApiResponse.ok(
service.update(id, thumbnail, detail, popup, link, title, isPopup),
"수정되었습니다."
)
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
fun deleteEvent(@PathVariable id: Long) = ApiResponse.ok(service.delete(id), "삭제되었습니다.")
}

View File

@ -10,6 +10,7 @@ interface EventRepository : JpaRepository<Event, Long>, EventQueryRepository
interface EventQueryRepository {
fun getEventList(): List<EventItem>
fun getMainEventPopup(): EventItem?
}
@Repository
@ -32,4 +33,27 @@ class EventQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Even
.orderBy(event.id.desc())
.fetch()
}
override fun getMainEventPopup(): EventItem? {
return queryFactory
.select(
QEventItem(
event.id,
event.title,
event.thumbnailImage,
event.detailImage,
event.popupImage,
event.link,
event.isPopup
)
)
.from(event)
.where(
event.isActive.isTrue
.and(event.isPopup.isTrue)
.and(event.popupImage.isNotNull)
)
.orderBy(event.id.desc())
.fetchFirst()
}
}

View File

@ -1,12 +1,22 @@
package kr.co.vividnext.sodalive.event
import com.amazonaws.services.s3.model.ObjectMetadata
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
class EventService(
private val repository: EventRepository,
private val s3Uploader: S3Uploader,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
@ -32,4 +42,164 @@ class EventService(
return GetEventResponse(0, eventList)
}
fun getEventPopup(): EventItem? {
val eventPopup = repository.getMainEventPopup()
if (eventPopup != null) {
if (!eventPopup.thumbnailImageUrl.startsWith("https://")) {
eventPopup.thumbnailImageUrl = "$cloudFrontHost/${eventPopup.thumbnailImageUrl}"
}
if (eventPopup.detailImageUrl != null && !eventPopup.detailImageUrl!!.startsWith("https://")) {
eventPopup.detailImageUrl = "$cloudFrontHost/${eventPopup.detailImageUrl}"
}
if (eventPopup.popupImageUrl != null && !eventPopup.popupImageUrl!!.startsWith("https://")) {
eventPopup.popupImageUrl = "$cloudFrontHost/${eventPopup.popupImageUrl}"
}
}
return eventPopup
}
@Transactional
fun save(
thumbnail: MultipartFile,
detail: MultipartFile? = null,
popup: MultipartFile? = null,
link: String? = null,
title: String? = null,
isPopup: Boolean
): Long {
if (detail == null && link.isNullOrBlank()) throw SodaException("상세이미지 혹은 링크를 등록하세요")
val event = repository.save(
Event(
thumbnailImage = "",
detailImage = null,
popupImage = null,
link = link,
title = title,
isPopup = isPopup
)
)
var metadata = ObjectMetadata()
metadata.contentLength = thumbnail.size
val thumbnailImagePath = s3Uploader.upload(
inputStream = thumbnail.inputStream,
bucket = bucket,
filePath = "event/${event.id}/${generateFileName()}",
metadata = metadata
)
val detailImagePath = if (detail != null) {
metadata = ObjectMetadata()
metadata.contentLength = detail.size
s3Uploader.upload(
inputStream = detail.inputStream,
bucket = bucket,
filePath = "event/${event.id}/${generateFileName()}",
metadata = metadata
)
} else {
null
}
val popupImagePath = if (popup != null) {
metadata = ObjectMetadata()
metadata.contentLength = popup.size
s3Uploader.upload(
inputStream = popup.inputStream,
bucket = bucket,
filePath = "event/${event.id}/${generateFileName()}",
metadata = metadata
)
} else {
null
}
event.thumbnailImage = thumbnailImagePath
event.detailImage = detailImagePath
event.popupImage = popupImagePath
return event.id ?: throw SodaException("이벤트 등록을 하지 못했습니다.")
}
@Transactional
fun update(
id: Long,
thumbnail: MultipartFile? = null,
detail: MultipartFile? = null,
popup: MultipartFile? = null,
link: String? = null,
title: String? = null,
isPopup: Boolean? = null
) {
if (id <= 0) throw SodaException("잘못된 요청입니다.")
if (thumbnail == null && detail == null && link.isNullOrBlank() && title.isNullOrBlank()) {
throw SodaException("수정할 내용을 입력하세요.")
}
val event = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.")
if (thumbnail != null) {
val metadata = ObjectMetadata()
metadata.contentLength = thumbnail.size
event.thumbnailImage = s3Uploader.upload(
inputStream = thumbnail.inputStream,
bucket = bucket,
filePath = "event/${event.id}/${generateFileName()}"
)
}
if (detail != null) {
val metadata = ObjectMetadata()
metadata.contentLength = detail.size
event.detailImage = s3Uploader.upload(
inputStream = detail.inputStream,
bucket = bucket,
filePath = "event/${event.id}/${generateFileName()}"
)
}
if (popup != null) {
val metadata = ObjectMetadata()
metadata.contentLength = popup.size
event.popupImage = s3Uploader.upload(
inputStream = popup.inputStream,
bucket = bucket,
filePath = "event/${event.id}/${generateFileName()}"
)
}
if (!link.isNullOrBlank() && event.link != link) {
event.link = link
}
if (!title.isNullOrBlank() && event.title != title) {
event.title = title
}
if (isPopup != null) {
event.isPopup = isPopup
}
}
@Transactional
fun delete(id: Long) {
if (id <= 0) throw SodaException("잘못된 요청입니다.")
val event = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.")
event.isActive = false
}
}

View File

@ -0,0 +1,5 @@
package kr.co.vividnext.sodalive.extensions
import java.text.DecimalFormat
fun Int.moneyFormat(): String = DecimalFormat("###,###").format(this)

View File

@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.faq
data class CreateFaqRequest(
val question: String,
val answer: String,
val category: String
)

View File

@ -0,0 +1,22 @@
package kr.co.vividnext.sodalive.faq
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
@Entity
data class Faq(
@Column(nullable = false)
var question: String,
@Column(columnDefinition = "TEXT", nullable = false)
var answer: String,
var isActive: Boolean = true,
val orders: Int = 1
) : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "category_id", nullable = false)
var category: FaqCategory? = null
}

View File

@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.faq
import kr.co.vividnext.sodalive.common.BaseEntity
import javax.persistence.Column
import javax.persistence.Entity
@Entity
data class FaqCategory(
@Column(nullable = false)
val category: String,
val isActive: Boolean = true,
val orders: Int = 1
) : BaseEntity()

View File

@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.faq
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface FaqCategoryRepository : JpaRepository<FaqCategory, Long>

View File

@ -0,0 +1,41 @@
package kr.co.vividnext.sodalive.faq
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestParam
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/faq")
class FaqController(private val service: FaqService) {
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
fun createFaq(@RequestBody request: CreateFaqRequest) = ApiResponse.ok(
service.save(request),
"등록되었습니다."
)
@PutMapping
@PreAuthorize("hasRole('ADMIN')")
fun modifyFaq(@RequestBody request: ModifyFaqRequest) = ApiResponse.ok(
service.modify(request),
"수정되었습니다."
)
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
fun deleteCoin(@PathVariable id: Long) = ApiResponse.ok(service.delete(id), "삭제되었습니다.")
@GetMapping
fun getFaqList(@RequestParam("category") category: String) = ApiResponse.ok(service.getFaqList(category))
@GetMapping("/category")
fun getFaqCategoryList() = ApiResponse.ok(service.getCategoryList())
}

View File

@ -0,0 +1,64 @@
package kr.co.vividnext.sodalive.faq
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.faq.QFaq.faq
import kr.co.vividnext.sodalive.faq.QFaqCategory.faqCategory
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface FaqRepository : JpaRepository<Faq, Long>
@Repository
class FaqQueryRepository(private val queryFactory: JPAQueryFactory) {
fun getCategory(category: String): FaqCategory? {
return queryFactory
.selectFrom(faqCategory)
.where(
faqCategory.isActive.isTrue
.and(faqCategory.category.eq(category))
)
.fetchFirst()
}
fun getCategoryList(): List<String> {
return queryFactory
.select(faqCategory.category)
.from(faqCategory)
.where(faqCategory.isActive.isTrue)
.orderBy(faqCategory.orders.desc())
.fetch()
}
fun getFaqList(category: String): List<GetFaqResponseItem> {
return queryFactory
.select(
QGetFaqResponseItem(
faq.id,
Expressions.asString(category),
faq.question,
faq.answer
)
)
.from(faq)
.innerJoin(faq.category, faqCategory)
.where(
faq.isActive.isTrue
.and(faqCategory.isActive.isTrue)
.and(faqCategory.category.eq(category))
)
.orderBy(faq.orders.desc())
.fetch()
}
fun getFaq(id: Long): Faq? {
return queryFactory
.selectFrom(faq)
.where(
faq.isActive.isTrue
.and(faq.id.eq(id))
)
.fetchFirst()
}
}

View File

@ -0,0 +1,62 @@
package kr.co.vividnext.sodalive.faq
import kr.co.vividnext.sodalive.common.SodaException
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
class FaqService(
private val repository: FaqRepository,
private val queryRepository: FaqQueryRepository
) {
@Transactional
fun save(request: CreateFaqRequest): Long {
if (request.question.isBlank()) throw SodaException("질문을 입력하세요.")
if (request.answer.isBlank()) throw SodaException("답변을 입력하세요.")
if (request.category.isBlank()) throw SodaException("카테고리를 선택하세요.")
val category = queryRepository.getCategory(request.category)
?: throw SodaException("잘못된 카테고리 입니다.")
val faq = Faq(request.question, request.answer)
faq.category = category
return repository.save(faq).id!!
}
@Transactional
fun modify(request: ModifyFaqRequest) {
val faq = queryRepository.getFaq(request.id)
?: throw SodaException("잘못된 요청입니다.")
if (request.question != null) {
if (request.question.isBlank()) throw SodaException("질문을 입력하세요.")
faq.question = request.question
}
if (request.answer != null) {
if (request.answer.isBlank()) throw SodaException("답변을 입력하세요.")
faq.answer = request.answer
}
if (request.category != null) {
if (request.category.isBlank()) throw SodaException("카테고리를 선택하세요.")
val category = queryRepository.getCategory(request.category) ?: throw SodaException("잘못된 카테고리 입니다.")
faq.category = category
}
}
@Transactional
fun delete(id: Long) {
if (id <= 0) throw SodaException("잘못된 요청입니다.")
val faq = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.")
faq.isActive = false
}
fun getFaqList(category: String) = queryRepository.getFaqList(category)
fun getCategoryList() = queryRepository.getCategoryList()
}

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.faq
import com.querydsl.core.annotations.QueryProjection
data class GetFaqResponseItem @QueryProjection constructor(
val id: Long,
val category: String,
val question: String,
val answer: String
)

View File

@ -0,0 +1,8 @@
package kr.co.vividnext.sodalive.faq
data class ModifyFaqRequest(
val id: Long,
val question: String? = null,
val answer: String? = null,
val category: String? = null
)

View File

@ -11,8 +11,6 @@ import javax.persistence.ManyToOne
@Entity
data class RecommendLiveCreatorBanner(
@Column(nullable = false)
var image: String,
@Column(nullable = false)
var startDate: LocalDateTime,
@Column(nullable = false)
@ -20,7 +18,9 @@ data class RecommendLiveCreatorBanner(
@Column(nullable = false)
var isAdult: Boolean = false,
@Column(nullable = false)
var orders: Int = 1
var orders: Int = 1,
@Column(nullable = true)
var image: String? = null
) : BaseEntity() {
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "creator_id", nullable = false)

View File

@ -0,0 +1,7 @@
package kr.co.vividnext.sodalive.live.recommend
import org.springframework.data.jpa.repository.JpaRepository
import org.springframework.stereotype.Repository
@Repository
interface RecommendLiveCreatorBannerRepository : JpaRepository<RecommendLiveCreatorBanner, Long>

View File

@ -792,7 +792,7 @@ class LiveRoomService(
val host = room.member ?: throw SodaException("잘못된 요청입니다.")
if (host.role != MemberRole.CREATOR) {
throw SodaException("비비드넥스트와 계약한\n요즘친구에게만 후원을 하실 수 있습니다.")
throw SodaException("비비드넥스트와 계약한\n크리에이터에게만 후원을 하실 수 있습니다.")
}
canPaymentService.spendCan(

View File

@ -1,12 +1,17 @@
package kr.co.vividnext.sodalive.live.tag
import kr.co.vividnext.sodalive.admin.member.tag.UpdateTagOrdersRequest
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import org.springframework.security.access.prepost.PreAuthorize
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.PathVariable
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RequestPart
import org.springframework.web.bind.annotation.RestController
@ -22,6 +27,24 @@ class LiveTagController(private val service: LiveTagService) {
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.enrollmentLiveTag(image, requestString), "등록되었습니다.")
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
fun deleteSudaTag(@PathVariable id: Long) = ApiResponse.ok(service.deleteTag(id), "삭제되었습니다.")
@PutMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
fun modifySudaTag(
@PathVariable id: Long,
@RequestPart("image") image: MultipartFile?,
@RequestPart("request") requestString: String
) = ApiResponse.ok(service.modifyTag(id, image, requestString), "수정되었습니다.")
@PutMapping("/orders")
@PreAuthorize("hasRole('ADMIN')")
fun updateTagOrders(
@RequestBody request: UpdateTagOrdersRequest
) = ApiResponse.ok(service.updateTagOrders(request.ids), "수정되었습니다.")
@GetMapping
fun getTags(
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?

View File

@ -7,7 +7,9 @@ import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.utils.generateFileName
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
@ -22,6 +24,7 @@ class LiveTagService(
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
@Transactional
fun enrollmentLiveTag(image: MultipartFile, requestString: String) {
val request = objectMapper.readValue(requestString, CreateLiveTagRequest::class.java)
tagExistCheck(request)
@ -42,6 +45,50 @@ class LiveTagService(
tag.image = tagImagePath
}
@Transactional
fun deleteTag(id: Long) {
val tag = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.")
tag.tag = "${tag.tag}_deleted"
tag.isActive = false
}
@Transactional
fun modifyTag(id: Long, image: MultipartFile?, requestString: String) {
val tag = repository.findByIdOrNull(id)
?: throw SodaException("잘못된 요청입니다.")
val request = objectMapper.readValue(requestString, CreateLiveTagRequest::class.java)
tag.tag = request.tag
if (image != null) {
val metadata = ObjectMetadata()
metadata.contentLength = image.size
val tagImageFileName = generateFileName(prefix = "${tag.id}-")
val tagImagePath = s3Uploader.upload(
inputStream = image.inputStream,
bucket = coverImageBucket,
filePath = "live_cover/${tag.id}/$tagImageFileName",
metadata = metadata
)
tag.image = tagImagePath
}
}
@Transactional
fun updateTagOrders(ids: List<Long>) {
for (index in ids.indices) {
val tag = repository.findByIdOrNull(ids[index])
if (tag != null) {
tag.orders = index + 1
}
}
}
fun getTags(member: Member): List<GetLiveTagResponse> {
return repository.getTags(role = member.role, isAdult = member.auth != null, cloudFrontHost = cloudFrontHost)
}

View File

@ -14,6 +14,7 @@ interface MemberRepository : JpaRepository<Member, Long>, MemberQueryRepository
interface MemberQueryRepository {
fun findByPushToken(pushToken: String): List<Member>
fun findByNicknameAndOtherCondition(nickname: String, memberId: Long): List<Member>
fun findCreatorByIdOrNull(memberId: Long): Member?
}
@Repository
@ -36,4 +37,14 @@ class MemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : Mem
)
.fetch()
}
override fun findCreatorByIdOrNull(memberId: Long): Member? {
return queryFactory
.selectFrom(member)
.where(
member.id.eq(memberId)
.and(member.role.eq(MemberRole.CREATOR))
)
.fetchFirst()
}
}

View File

@ -0,0 +1,33 @@
package kr.co.vividnext.sodalive.member.stipulation
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/stplat")
class StipulationController(private val service: StipulationService) {
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
fun enrollment(@RequestBody request: StipulationDto) = ApiResponse.ok(
service.enrollment(request),
"등록되었습니다."
)
@PostMapping("/modify")
@PreAuthorize("hasRole('ADMIN')")
fun modify(@RequestBody request: StipulationModifyRequest) = ApiResponse.ok(
service.modify(request),
"수정되었습니다."
)
@GetMapping("/terms_of_service")
fun getTermsOfService() = ApiResponse.ok(service.getTermsOfService())
@GetMapping("/privacy_policy")
fun getPrivacyPolicy() = ApiResponse.ok(service.getPrivacyPolicy())
}

View File

@ -0,0 +1,11 @@
package kr.co.vividnext.sodalive.member.stipulation
data class StipulationDto(
val id: Long = 0,
val title: String,
val description: String
) {
constructor(stipulation: Stipulation) : this(stipulation.id!!, stipulation.title, stipulation.description)
fun toEntity() = Stipulation(title, description)
}

View File

@ -0,0 +1,6 @@
package kr.co.vividnext.sodalive.member.stipulation
data class StipulationModifyRequest(
val id: Long,
val description: String
)

View File

@ -0,0 +1,38 @@
package kr.co.vividnext.sodalive.member.stipulation
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.stipulation.StipulationIds.PRIVACY_POLICY_ID
import kr.co.vividnext.sodalive.member.stipulation.StipulationIds.TERMS_OF_SERVICE_ID
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
@Service
@Transactional(readOnly = true)
class StipulationService(private val repository: StipulationRepository) {
fun enrollment(request: StipulationDto) {
repository.save(request.toEntity())
}
fun getTermsOfService(): StipulationDto {
val stipulation = repository.findByIdOrNull(TERMS_OF_SERVICE_ID)
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
return StipulationDto(stipulation)
}
fun getPrivacyPolicy(): StipulationDto {
val stipulation = repository.findByIdOrNull(PRIVACY_POLICY_ID)
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
return StipulationDto(stipulation)
}
@Transactional
fun modify(request: StipulationModifyRequest) {
val stipulation = repository.findByIdOrNull(request.id)
?: throw SodaException("잘못된 요청입니다\n앱 종료 후 다시 시도해 주세요.")
stipulation.description = request.description
}
}

View File

@ -0,0 +1,9 @@
package kr.co.vividnext.sodalive.member.tag
import com.querydsl.core.annotations.QueryProjection
data class GetMemberTagResponse @QueryProjection constructor(
val id: Long,
val tag: String,
val image: String
)

View File

@ -0,0 +1,13 @@
package kr.co.vividnext.sodalive.member.tag
import kr.co.vividnext.sodalive.common.ApiResponse
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/member/tag")
class MemberTagController(private val service: MemberTagService) {
@GetMapping
fun getTags() = ApiResponse.ok(service.getTags())
}

View File

@ -0,0 +1,34 @@
package kr.co.vividnext.sodalive.member.tag
import com.querydsl.jpa.impl.JPAQueryFactory
import kr.co.vividnext.sodalive.member.tag.QCreatorTag.creatorTag
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.jpa.repository.JpaRepository
interface MemberTagRepository : JpaRepository<CreatorTag, Long>, MemberTagQueryRepository
interface MemberTagQueryRepository {
fun getTags(): List<GetMemberTagResponse>
}
class MemberTagQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) : MemberTagQueryRepository {
override fun getTags(): List<GetMemberTagResponse> {
return queryFactory
.select(
QGetMemberTagResponse(
creatorTag.id,
creatorTag.tag,
creatorTag.image.prepend("/").prepend(cloudFrontHost)
)
)
.from(creatorTag)
.where(creatorTag.isActive.eq(true))
.orderBy(creatorTag.orders.asc())
.fetch()
}
}

View File

@ -0,0 +1,10 @@
package kr.co.vividnext.sodalive.member.tag
import org.springframework.stereotype.Service
@Service
class MemberTagService(private val repository: MemberTagRepository) {
fun getTags(): List<GetMemberTagResponse> {
return repository.getTags()
}
}