diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/calculate/AdminCalculateQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/calculate/AdminCalculateQueryRepository.kt index f94b641..2f61759 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/calculate/AdminCalculateQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/calculate/AdminCalculateQueryRepository.kt @@ -65,6 +65,7 @@ class AdminCalculateQueryRepository(private val queryFactory: JPAQueryFactory) { .where( order.createdAt.goe(startDate) .and(order.createdAt.loe(endDate)) + .and(order.isActive.isTrue) ) .groupBy(audioContent.id, order.type, orderFormattedDate, order.can) .orderBy(member.id.desc(), orderFormattedDate.desc(), audioContent.id.asc()) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt index d86afd3..f631261 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt @@ -105,6 +105,7 @@ class AudioContentController(private val service: AudioContentService) { fun getAudioContentList( @RequestParam("creator-id") creatorId: Long, @RequestParam("sort-type", required = false) sortType: SortType = SortType.NEWEST, + @RequestParam("category-id", required = false) categoryId: Long? = 0, @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, pageable: Pageable ) = run { @@ -114,6 +115,7 @@ class AudioContentController(private val service: AudioContentService) { service.getAudioContentList( creatorId = creatorId, sortType = sortType, + categoryId = categoryId ?: 0, member = member, offset = pageable.offset, limit = pageable.pageSize.toLong() diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt index 4e90a0e..ff06f60 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt @@ -4,6 +4,7 @@ import com.querydsl.core.types.dsl.Expressions import com.querydsl.jpa.impl.JPAQueryFactory import kr.co.vividnext.sodalive.content.QAudioContent.audioContent import kr.co.vividnext.sodalive.content.QBundleAudioContent.bundleAudioContent +import kr.co.vividnext.sodalive.content.category.QCategoryContent.categoryContent import kr.co.vividnext.sodalive.content.comment.QAudioContentComment.audioContentComment import kr.co.vividnext.sodalive.content.like.QAudioContentLike.audioContentLike import kr.co.vividnext.sodalive.content.main.GetAudioContentMainItem @@ -37,6 +38,7 @@ interface AudioContentQueryRepository { coverImageHost: String, isAdult: Boolean = false, sortType: SortType = SortType.NEWEST, + categoryId: Long = 0, offset: Long = 0, limit: Long = 10 ): List @@ -144,6 +146,7 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) coverImageHost: String, isAdult: Boolean, sortType: SortType, + categoryId: Long, offset: Long, limit: Long ): List { @@ -163,6 +166,10 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) where = where.and(audioContent.isAdult.isFalse) } + if (categoryId > 0) { + where = where.and(categoryContent.category.id.eq(categoryId)) + } + return queryFactory .select( QGetAudioContentListItem( @@ -180,6 +187,8 @@ class AudioContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) ) ) .from(audioContent) + .leftJoin(categoryContent) + .on(audioContent.id.eq(categoryContent.content.id).and(categoryContent.isActive.ne(false))) .leftJoin(pinContent) .on(audioContent.id.eq(pinContent.content.id).and(pinContent.isActive.ne(false))) .where(where) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt index 9c52cb4..832a9f9 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt @@ -624,6 +624,7 @@ class AudioContentService( creatorId: Long, sortType: SortType, member: Member, + categoryId: Long = 0, offset: Long, limit: Long ): GetAudioContentListResponse { @@ -637,6 +638,7 @@ class AudioContentService( coverImageHost = coverImageHost, isAdult = member.auth != null, sortType = sortType, + categoryId = categoryId, offset = offset, limit = limit ) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/Category.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/Category.kt new file mode 100644 index 0000000..6125131 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/Category.kt @@ -0,0 +1,19 @@ +package kr.co.vividnext.sodalive.content.category + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class Category( + var title: String, + var orders: Int = 1, + var isActive: Boolean = true +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryContent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryContent.kt new file mode 100644 index 0000000..2363226 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryContent.kt @@ -0,0 +1,22 @@ +package kr.co.vividnext.sodalive.content.category + +import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.content.AudioContent +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne + +@Entity +data class CategoryContent( + var orders: Int = 1, + var isActive: Boolean = true +) : BaseEntity() { + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id", nullable = false) + var content: AudioContent? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "category_id", nullable = false) + var category: Category? = null +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryContentRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryContentRepository.kt new file mode 100644 index 0000000..e560d60 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryContentRepository.kt @@ -0,0 +1,32 @@ +package kr.co.vividnext.sodalive.content.category + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.category.QCategoryContent.categoryContent +import org.springframework.data.jpa.repository.JpaRepository + +interface CategoryContentRepository : JpaRepository, CategoryContentQueryRepository + +interface CategoryContentQueryRepository { + fun findByContentIdAndCategoryId(contentId: Long, categoryId: Long): CategoryContent? + fun deleteByCategoryId(categoryId: Long) +} + +class CategoryContentQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CategoryContentQueryRepository { + override fun findByContentIdAndCategoryId(contentId: Long, categoryId: Long): CategoryContent? { + return queryFactory + .selectFrom(categoryContent) + .where( + categoryContent.content.id.eq(contentId) + .and(categoryContent.category.id.eq(categoryId)) + ) + .fetchFirst() + } + + override fun deleteByCategoryId(categoryId: Long) { + queryFactory + .update(categoryContent) + .set(categoryContent.isActive, false) + .where(categoryContent.category.id.eq(categoryId)) + .execute() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryController.kt new file mode 100644 index 0000000..b3db249 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryController.kt @@ -0,0 +1,63 @@ +package kr.co.vividnext.sodalive.content.category + +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.RequestParam +import org.springframework.web.bind.annotation.RestController + +@RestController +@RequestMapping("/category") +class CategoryController(private val service: CategoryService) { + @PostMapping + @PreAuthorize("hasRole('CREATOR')") + fun createCategory( + @RequestBody request: CreateCategoryRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.createCategory(request = request, member = member)) + } + + @PutMapping + @PreAuthorize("hasRole('CREATOR')") + fun modifyCategory( + @RequestBody request: ModifyCategoryRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.modifyCategory(request = request, member = member)) + } + + @DeleteMapping("/{id}") + @PreAuthorize("hasRole('CREATOR')") + fun deleteCategory( + @PathVariable("id") categoryId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.deleteCategory(categoryId = categoryId, member = member)) + } + + @GetMapping + fun getCategoryList( + @RequestParam creatorId: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getCategoryList(creatorId = creatorId, memberId = member.id!!)) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryRepository.kt new file mode 100644 index 0000000..eacc9bd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryRepository.kt @@ -0,0 +1,45 @@ +package kr.co.vividnext.sodalive.content.category + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.category.QCategory.category +import org.springframework.data.jpa.repository.JpaRepository + +interface CategoryRepository : JpaRepository, CategoryQueryRepository + +interface CategoryQueryRepository { + fun findByTitleAndMemberId(title: String, memberId: Long): Category? + + fun findByIdAndMemberId(categoryId: Long, memberId: Long): Category? + + fun findByCreatorId(creatorId: Long): List +} + +class CategoryQueryRepositoryImpl(private val queryFactory: JPAQueryFactory) : CategoryQueryRepository { + override fun findByTitleAndMemberId(title: String, memberId: Long): Category? { + return queryFactory + .selectFrom(category) + .where( + category.title.eq(title) + .and(category.member.id.eq(memberId)) + ) + .fetchFirst() + } + + override fun findByIdAndMemberId(categoryId: Long, memberId: Long): Category? { + return queryFactory + .selectFrom(category) + .where( + category.id.eq(categoryId) + .and(category.member.id.eq(memberId)) + ) + .fetchFirst() + } + + override fun findByCreatorId(creatorId: Long): List { + return queryFactory + .select(QGetCategoryListResponse(category.id, category.title)) + .from(category) + .where(category.member.id.eq(creatorId)) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryService.kt new file mode 100644 index 0000000..3afe603 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CategoryService.kt @@ -0,0 +1,99 @@ +package kr.co.vividnext.sodalive.content.category + +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.content.AudioContentRepository +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.member.block.BlockMemberRepository +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +class CategoryService( + private val repository: CategoryRepository, + private val contentRepository: AudioContentRepository, + private val blockMemberRepository: BlockMemberRepository, + private val categoryContentRepository: CategoryContentRepository +) { + @Transactional + fun createCategory(request: CreateCategoryRequest, member: Member) { + validateTitle(title = request.title) + val category = repository.findByTitleAndMemberId(title = request.title, memberId = member.id!!) + ?: repository.save( + Category(title = request.title).apply { + this.member = member + } + ) + category.isActive = true + + for (contentId in request.contentIdList) { + val content = contentRepository.findByIdAndActive(contentId = contentId) + ?: continue + + val categoryContent = categoryContentRepository.findByContentIdAndCategoryId( + contentId = contentId, + categoryId = category.id!! + ) ?: categoryContentRepository.save( + CategoryContent().apply { + this.content = content + this.category = category + } + ) + categoryContent.isActive = true + } + } + + @Transactional + fun modifyCategory(request: ModifyCategoryRequest, member: Member) { + val category = repository.findByIdAndMemberId(categoryId = request.categoryId, memberId = member.id!!) + ?: throw SodaException("잘못된 요청입니다.") + + if (!request.title.isNullOrBlank()) { + validateTitle(title = request.title) + category.title = request.title + } + + for (contentId in request.addContentIdList) { + val content = contentRepository.findByIdAndActive(contentId = contentId) + ?: continue + + val categoryContent = categoryContentRepository.findByContentIdAndCategoryId( + contentId = contentId, + categoryId = category.id!! + ) ?: categoryContentRepository.save( + CategoryContent().apply { + this.content = content + this.category = category + } + ) + categoryContent.isActive = true + } + + for (contentId in request.removeContentIdList) { + val categoryContent = categoryContentRepository.findByContentIdAndCategoryId( + contentId = contentId, + categoryId = category.id!! + ) ?: continue + categoryContent.isActive = false + } + } + + @Transactional + fun deleteCategory(categoryId: Long, member: Member) { + val category = repository.findByIdAndMemberId(categoryId = categoryId, memberId = member.id!!) + ?: throw SodaException("잘못된 요청입니다.") + category.isActive = false + + categoryContentRepository.deleteByCategoryId(categoryId = categoryId) + } + + fun getCategoryList(creatorId: Long, memberId: Long): List { + val isBlocked = blockMemberRepository.isBlocked(blockedMemberId = memberId, memberId = creatorId) + if (isBlocked) throw SodaException("잘못된 접근입니다.") + + return repository.findByCreatorId(creatorId = creatorId) + } + + private fun validateTitle(title: String) { + if (title.length < 2) throw SodaException("카테고리명은 2글자 이상 입력하세요") + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CreateCategoryRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CreateCategoryRequest.kt new file mode 100644 index 0000000..7484b3c --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/CreateCategoryRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.content.category + +data class CreateCategoryRequest( + val title: String, + val contentIdList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/GetCategoryListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/GetCategoryListResponse.kt new file mode 100644 index 0000000..3e575c8 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/GetCategoryListResponse.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.content.category + +import com.querydsl.core.annotations.QueryProjection + +data class GetCategoryListResponse @QueryProjection constructor( + val categoryId: Long, + val category: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/content/category/ModifyCategoryRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/ModifyCategoryRequest.kt new file mode 100644 index 0000000..5bd5b37 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/content/category/ModifyCategoryRequest.kt @@ -0,0 +1,8 @@ +package kr.co.vividnext.sodalive.content.category + +data class ModifyCategoryRequest( + val categoryId: Long, + val title: String?, + val addContentIdList: List, + val removeContentIdList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/calculate/CreatorAdminCalculateQueryRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/calculate/CreatorAdminCalculateQueryRepository.kt index fb1fc98..2a7dc02 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/calculate/CreatorAdminCalculateQueryRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/calculate/CreatorAdminCalculateQueryRepository.kt @@ -116,16 +116,14 @@ class CreatorAdminCalculateQueryRepository(private val queryFactory: JPAQueryFac order.can.sum() ) ) - .from(useCanCalculate) - .innerJoin(useCanCalculate.useCan, useCan) - .innerJoin(useCan.order, order) + .from(order) .innerJoin(order.audioContent, audioContent) .innerJoin(audioContent.member, member) .where( - useCanCalculate.status.eq(UseCanCalculateStatus.RECEIVED) - .and(useCanCalculate.recipientCreatorId.eq(memberId)) - .and(order.createdAt.goe(startDate)) + order.createdAt.goe(startDate) .and(order.createdAt.loe(endDate)) + .and(order.isActive.isTrue) + .and(order.creator.id.eq(memberId)) ) .groupBy(audioContent.id, order.type, orderFormattedDate, order.can) .offset(offset)