From 14b25bdfc306713d5452411848197cdcd05d86dd Mon Sep 17 00:00:00 2001 From: Klaus Date: Mon, 7 Aug 2023 01:23:42 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B4=80=EB=A6=AC=EC=9E=90=20-=20=EC=BD=98?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EB=A6=AC=EC=8A=A4=ED=8A=B8,=20=EC=BD=98?= =?UTF-8?q?=ED=85=90=EC=B8=A0=20=EB=B0=B0=EB=84=88=EA=B4=80=EB=A6=AC,=20?= =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=ED=81=90=EB=A0=88=EC=9D=B4?= =?UTF-8?q?=EC=85=98=20=EA=B4=80=EB=A6=AC=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../admin/content/AdminContentController.kt | 30 ++++ .../admin/content/AdminContentRepository.kt | 119 +++++++++++++++ .../admin/content/AdminContentService.kt | 121 +++++++++++++++ .../content/GetAdminContentListResponse.kt | 26 ++++ .../content/UpdateAdminContentRequest.kt | 12 ++ .../banner/AdminContentBannerController.kt | 37 +++++ .../banner/AdminContentBannerRepository.kt | 46 ++++++ .../banner/AdminContentBannerService.kt | 144 ++++++++++++++++++ .../banner/CreateContentBannerRequest.kt | 11 ++ .../banner/GetAdminContentBannerResponse.kt | 16 ++ .../banner/UpdateBannerOrdersRequest.kt | 5 + .../banner/UpdateContentBannerRequest.kt | 13 ++ .../AdminContentCurationController.kt | 33 ++++ .../AdminContentCurationRepository.kt | 48 ++++++ .../curation/AdminContentCurationService.kt | 60 ++++++++ .../curation/AudioContentCurationRequest.kt | 19 +++ .../GetAdminContentCurationResponse.kt | 10 ++ .../admin/member/AdminMemberController.kt | 3 + .../admin/member/AdminMemberRepository.kt | 17 +++ .../admin/member/AdminMemberService.kt | 4 + .../member/GetAdminCreatorAllListResponse.kt | 8 + 21 files changed, 782 insertions(+) create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/CreateContentBannerRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/GetAdminContentBannerResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateBannerOrdersRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateContentBannerRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationController.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationRepository.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationService.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AudioContentCurationRequest.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/GetAdminContentCurationResponse.kt create mode 100644 src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminCreatorAllListResponse.kt diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentController.kt new file mode 100644 index 0000000..2b951e6 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentController.kt @@ -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)) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt new file mode 100644 index 0000000..1db3529 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentRepository.kt @@ -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, AdminAudioContentQueryRepository + +interface AdminAudioContentQueryRepository { + fun getAudioContentTotalCount(searchWord: String = ""): Int + fun getAudioContentList(offset: Long, limit: Long, searchWord: String = ""): List + fun getHashTagList(audioContentId: Long): List +} + +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 { + 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 { + 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, + 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 + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt new file mode 100644 index 0000000..677e3b9 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/AdminContentService.kt @@ -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 + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt new file mode 100644 index 0000000..81b88b8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/GetAdminContentListResponse.kt @@ -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 +) + +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 = "" +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt new file mode 100644 index 0000000..376485b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/UpdateAdminContentRequest.kt @@ -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? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerController.kt new file mode 100644 index 0000000..1324585 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerController.kt @@ -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()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerRepository.kt new file mode 100644 index 0000000..aa654dd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerRepository.kt @@ -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, AdminContentBannerQueryRepository + +interface AdminContentBannerQueryRepository { + fun getAudioContentMainBannerList(): List +} + +class AdminContentBannerQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory, + @Value("\${cloud.aws.cloud-front.host}") + private val cloudFrontHost: String +) : AdminContentBannerQueryRepository { + override fun getAudioContentMainBannerList(): List { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerService.kt new file mode 100644 index 0000000..b7e4837 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/AdminContentBannerService.kt @@ -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) { + for (index in ids.indices) { + val tag = repository.findByIdOrNull(ids[index]) + + if (tag != null) { + tag.orders = index + 1 + } + } + } + + fun getAudioContentMainBannerList(): List { + return repository.getAudioContentMainBannerList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/CreateContentBannerRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/CreateContentBannerRequest.kt new file mode 100644 index 0000000..3203366 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/CreateContentBannerRequest.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/GetAdminContentBannerResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/GetAdminContentBannerResponse.kt new file mode 100644 index 0000000..6618f2b --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/GetAdminContentBannerResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateBannerOrdersRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateBannerOrdersRequest.kt new file mode 100644 index 0000000..2dba5aa --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateBannerOrdersRequest.kt @@ -0,0 +1,5 @@ +package kr.co.vividnext.sodalive.admin.content.banner + +data class UpdateBannerOrdersRequest( + val ids: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateContentBannerRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateContentBannerRequest.kt new file mode 100644 index 0000000..fe97928 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/banner/UpdateContentBannerRequest.kt @@ -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? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationController.kt new file mode 100644 index 0000000..849697a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationController.kt @@ -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()) +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationRepository.kt new file mode 100644 index 0000000..a24fa77 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationRepository.kt @@ -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, + AdminContentCurationQueryRepository + +interface AdminContentCurationQueryRepository { + fun getAudioContentCurationList(): List + fun findByIdAndActive(id: Long): AudioContentCuration? +} + +@Repository +class AdminContentCurationQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : AdminContentCurationQueryRepository { + override fun getAudioContentCurationList(): List { + 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() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationService.kt new file mode 100644 index 0000000..b6d0c14 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AdminContentCurationService.kt @@ -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) { + for (index in ids.indices) { + val audioContentCuration = repository.findByIdOrNull(ids[index]) + + if (audioContentCuration != null) { + audioContentCuration.orders = index + 1 + } + } + } + + fun getContentCurationList(): List { + return repository.getAudioContentCurationList() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AudioContentCurationRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AudioContentCurationRequest.kt new file mode 100644 index 0000000..49b46d7 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/AudioContentCurationRequest.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/GetAdminContentCurationResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/GetAdminContentCurationResponse.kt new file mode 100644 index 0000000..2938cfd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/curation/GetAdminContentCurationResponse.kt @@ -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 +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt index ad6d0d1..43675a7 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt @@ -21,6 +21,9 @@ class AdminMemberController(private val service: AdminMemberService) { 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)) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt index 3dd53b0..4b1a2ed 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberRepository.kt @@ -14,6 +14,7 @@ interface AdminMemberQueryRepository { fun searchMember(searchWord: String, offset: Long, limit: Long, role: MemberRole? = null): List fun searchMemberTotalCount(searchWord: String, role: MemberRole? = null): Int + fun getCreatorAllList(): List } class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : AdminMemberQueryRepository { @@ -92,4 +93,20 @@ class AdminMemberQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) .fetch() .size } + + override fun getCreatorAllList(): List { + return queryFactory + .select( + QGetAdminCreatorAllListResponse( + member.id, + member.nickname + ) + ) + .from(member) + .where( + member.role.eq(MemberRole.CREATOR) + .and(member.isActive.isTrue) + ) + .fetch() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt index 15256d1..0550575 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberService.kt @@ -117,4 +117,8 @@ class AdminMemberService( } .toList() } + + fun getCreatorAllList(): List { + return repository.getCreatorAllList() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminCreatorAllListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminCreatorAllListResponse.kt new file mode 100644 index 0000000..30c006a --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminCreatorAllListResponse.kt @@ -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 +)