test #13

Merged
klaus merged 4 commits from test into main 2023-08-23 14:05:01 +00:00
10 changed files with 497 additions and 1 deletions

View File

@ -68,6 +68,7 @@ class SecurityConfig(
.antMatchers("/member/check/nickname").permitAll()
.antMatchers("/member/signup").permitAll()
.antMatchers("/member/login").permitAll()
.antMatchers("/creator-admin/member/login").permitAll()
.antMatchers("/member/forgot-password").permitAll()
.antMatchers("/stplat/terms_of_service").permitAll()
.antMatchers("/stplat/privacy_policy").permitAll()

View File

@ -10,6 +10,8 @@ class WebConfig : WebMvcConfigurer {
registry.addMapping("/**")
.allowedOrigins(
"http://localhost:8888",
"https://creator.sodalive.net",
"https://test-creator.sodalive.net",
"https://test-admin.sodalive.net",
"https://admin.sodalive.net"
)

View File

@ -0,0 +1,52 @@
package kr.co.vividnext.sodalive.creator.admin.content
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import org.springframework.data.domain.Pageable
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.GetMapping
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.RequestPart
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.multipart.MultipartFile
@RestController
@PreAuthorize("hasRole('CREATOR')")
@RequestMapping("/creator-admin/audio-content")
class CreatorAdminContentController(private val service: CreatorAdminContentService) {
@GetMapping("/list")
fun getAudioContentList(
pageable: Pageable,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.getAudioContentList(pageable, member))
}
@GetMapping("/search")
fun searchAudioContent(
@RequestParam(value = "search_word") searchWord: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?,
pageable: Pageable
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.searchAudioContent(searchWord, member, pageable))
}
@PutMapping
fun modifyAudioContent(
@RequestPart("coverImage", required = false) coverImage: MultipartFile? = null,
@RequestPart("request") requestString: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.updateAudioContent(coverImage, requestString, member))
}
}

View File

@ -0,0 +1,142 @@
package kr.co.vividnext.sodalive.creator.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.theme.QAudioContentTheme.audioContentTheme
import kr.co.vividnext.sodalive.member.QMember.member
import org.springframework.data.jpa.repository.JpaRepository
import java.time.LocalDateTime
interface CreatorAdminContentRepository : JpaRepository<AudioContent, Long>, CreatorAdminAudioContentQueryRepository
interface CreatorAdminAudioContentQueryRepository {
fun getAudioContentTotalCount(memberId: Long, searchWord: String = ""): Int
fun getAudioContentList(
memberId: Long,
offset: Long,
limit: Long,
searchWord: String = ""
): List<GetCreatorAdminContentListItem>
fun getHashTagList(audioContentId: Long): List<String>
fun getAudioContent(memberId: Long, audioContentId: Long): AudioContent?
}
class CreatorAdminAudioContentQueryRepositoryImpl(
private val queryFactory: JPAQueryFactory
) : CreatorAdminAudioContentQueryRepository {
override fun getAudioContentTotalCount(memberId: Long, searchWord: String): Int {
var where = audioContent.duration.isNotNull
.and(audioContent.member.isNotNull)
.and(audioContent.isActive.isTrue)
.and(audioContent.member.id.eq(memberId))
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(
memberId: Long,
offset: Long,
limit: Long,
searchWord: String
): List<GetCreatorAdminContentListItem> {
var where = audioContent.duration.isNotNull
.and(audioContent.member.isNotNull)
.and(audioContent.isActive.isTrue)
.and(audioContent.member.id.eq(memberId))
if (searchWord.trim().length > 1) {
where = where.and(
audioContent.title.contains(searchWord)
.or(audioContent.member.nickname.contains(searchWord))
)
}
return queryFactory
.select(
QGetCreatorAdminContentListItem(
audioContent.id,
audioContent.title,
audioContent.detail,
audioContent.coverImage,
audioContent.member!!.nickname,
audioContentTheme.theme,
audioContent.price,
audioContent.isAdult,
audioContent.isCommentAvailable,
audioContent.duration,
audioContent.content,
formattedDateExpression(audioContent.createdAt)
)
)
.from(audioContent)
.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()
}
override fun getAudioContent(memberId: Long, audioContentId: Long): AudioContent? {
return queryFactory
.selectFrom(audioContent)
.innerJoin(audioContent.member, member)
.where(
member.id.eq(memberId)
.and(audioContent.id.eq(audioContentId))
)
.orderBy(audioContent.id.desc())
.fetchFirst()
}
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,140 @@
package kr.co.vividnext.sodalive.creator.admin.content
import com.amazonaws.services.s3.model.ObjectMetadata
import com.fasterxml.jackson.databind.ObjectMapper
import kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFront
import kr.co.vividnext.sodalive.aws.s3.S3Uploader
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.domain.Pageable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import org.springframework.web.multipart.MultipartFile
@Service
class CreatorAdminContentService(
private val repository: CreatorAdminContentRepository,
private val audioContentCloudFront: AudioContentCloudFront,
private val objectMapper: ObjectMapper,
private val s3Uploader: S3Uploader,
@Value("\${cloud.aws.s3.bucket}")
private val bucket: String,
@Value("\${cloud.aws.cloud-front.host}")
private val coverImageHost: String
) {
fun getAudioContentList(pageable: Pageable, member: Member): GetCreatorAdminContentListResponse {
val totalCount = repository.getAudioContentTotalCount(memberId = member.id!!)
val audioContentAndThemeList = repository.getAudioContentList(
memberId = member.id!!,
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 GetCreatorAdminContentListResponse(totalCount, audioContentList)
}
fun searchAudioContent(searchWord: String, member: Member, pageable: Pageable): GetCreatorAdminContentListResponse {
if (searchWord.length < 2) throw SodaException("2글자 이상 입력하세요.")
val totalCount = repository.getAudioContentTotalCount(
memberId = member.id!!,
searchWord
)
val audioContentAndThemeList = repository.getAudioContentList(
memberId = member.id!!,
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 GetCreatorAdminContentListResponse(totalCount, audioContentList)
}
@Transactional
fun updateAudioContent(coverImage: MultipartFile?, requestString: String, member: Member) {
val request = objectMapper.readValue(requestString, UpdateCreatorAdminContentRequest::class.java)
val audioContent = repository.getAudioContent(memberId = member.id!!, audioContentId = request.id)
?: throw SodaException("잘못된 콘텐츠 입니다.")
if (coverImage != null) {
val metadata = ObjectMetadata()
metadata.contentLength = coverImage.size
val fileName = generateFileName()
val imagePath = s3Uploader.upload(
inputStream = coverImage.inputStream,
bucket = bucket,
filePath = "audio_content_cover/${request.id}/$fileName",
metadata = metadata
)
audioContent.coverImage = imagePath
}
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
}
}
}

View File

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

View File

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

View File

@ -0,0 +1,31 @@
package kr.co.vividnext.sodalive.creator.admin.member
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.member.Member
import kr.co.vividnext.sodalive.member.login.LoginRequest
import org.springframework.security.access.prepost.PreAuthorize
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.RequestHeader
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/creator-admin/member")
class CreatorAdminMemberController(private val service: CreatorAdminMemberService) {
@PostMapping("/login")
fun login(@RequestBody loginRequest: LoginRequest) = service.login(loginRequest)
@PostMapping("/logout")
@PreAuthorize("hasRole('CREATOR')")
fun logout(
@RequestHeader("Authorization") token: String,
@AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?
) = run {
if (member == null) throw SodaException("로그인 정보를 확인해주세요.")
ApiResponse.ok(service.logout(token.removePrefix("Bearer "), member.id!!))
}
}

View File

@ -0,0 +1,93 @@
package kr.co.vividnext.sodalive.creator.admin.member
import kr.co.vividnext.sodalive.common.ApiResponse
import kr.co.vividnext.sodalive.common.SodaException
import kr.co.vividnext.sodalive.jwt.TokenProvider
import kr.co.vividnext.sodalive.member.MemberRepository
import kr.co.vividnext.sodalive.member.MemberRole
import kr.co.vividnext.sodalive.member.login.LoginRequest
import kr.co.vividnext.sodalive.member.login.LoginResponse
import kr.co.vividnext.sodalive.member.token.MemberTokenRepository
import org.springframework.beans.factory.annotation.Value
import org.springframework.data.repository.findByIdOrNull
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder
import org.springframework.security.core.context.SecurityContextHolder
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.write
@Service
class CreatorAdminMemberService(
private val repository: MemberRepository,
private val tokenRepository: MemberTokenRepository,
private val tokenProvider: TokenProvider,
private val authenticationManagerBuilder: AuthenticationManagerBuilder,
@Value("\${cloud.aws.cloud-front.host}")
private val cloudFrontHost: String
) {
private val tokenLocks: MutableMap<Long, ReentrantReadWriteLock> = mutableMapOf()
fun login(request: LoginRequest): ApiResponse<LoginResponse> {
return ApiResponse.ok(
message = "로그인 되었습니다.",
data = login(request.email, request.password)
)
}
@Transactional
fun logout(token: String, memberId: Long) {
val member = repository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.")
member.pushToken = null
val lock = getOrCreateLock(memberId = memberId)
lock.write {
val memberToken = tokenRepository.findByIdOrNull(memberId)
?: throw SodaException("로그인 정보를 확인해주세요.")
memberToken.tokenSet.remove(token)
tokenRepository.save(memberToken)
}
}
private fun login(email: String, password: String): LoginResponse {
val member = repository.findByEmail(email = email) ?: throw SodaException("로그인 정보를 확인해주세요.")
if (!member.isActive) {
throw SodaException("탈퇴한 계정입니다.\n고객센터로 문의해 주시기 바랍니다.")
}
if (member.role != MemberRole.CREATOR) {
throw SodaException("로그인 정보를 확인해주세요.")
}
val authenticationToken = UsernamePasswordAuthenticationToken(email, password)
val authentication = authenticationManagerBuilder.`object`.authenticate(authenticationToken)
SecurityContextHolder.getContext().authentication = authentication
val jwt = tokenProvider.createToken(
authentication = authentication,
memberId = member.id!!
)
return LoginResponse(
userId = member.id!!,
token = jwt,
nickname = member.nickname,
email = member.email,
profileImage = if (member.profileImage != null) {
"$cloudFrontHost/${member.profileImage}"
} else {
"$cloudFrontHost/profile/default-profile.png"
}
)
}
private fun getOrCreateLock(memberId: Long): ReentrantReadWriteLock {
return tokenLocks.computeIfAbsent(memberId) { ReentrantReadWriteLock() }
}
}

View File

@ -12,6 +12,6 @@ import org.springframework.web.bind.annotation.RestController
@RequestMapping("/menu")
class MenuController(private val service: MenuService) {
@GetMapping
@PreAuthorize("hasAnyRole('AGENT', 'ADMIN')")
@PreAuthorize("hasAnyRole('AGENT', 'ADMIN', 'CREATOR')")
fun getMenus(@AuthenticationPrincipal user: User) = ApiResponse.ok(service.getMenus(user))
}