diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreController.kt index b0dd061..981d4a6 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreController.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreController.kt @@ -2,6 +2,7 @@ package kr.co.vividnext.sodalive.admin.content.series.genre 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 @@ -14,7 +15,7 @@ import org.springframework.web.bind.annotation.RestController class AdminContentSeriesGenreController(private val service: AdminContentSeriesGenreService) { @PostMapping fun createSeriesGenre(@RequestBody request: CreateSeriesGenreRequest) = - ApiResponse.ok(service.createSeriesGenre(genre = request.genre), "생성되었습니다.") + ApiResponse.ok(service.createSeriesGenre(genre = request.genre, isAdult = request.isAdult), "생성되었습니다.") @PutMapping fun modifySeriesGenre(@RequestBody request: ModifySeriesGenreRequest) = ApiResponse.ok( @@ -27,4 +28,7 @@ class AdminContentSeriesGenreController(private val service: AdminContentSeriesG service.modifySeriesGenreOrders(ids = request.ids), "수정되었습니다." ) + + @GetMapping + fun getSeriesGenreList() = ApiResponse.ok(service.getSeriesGenreList()) } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreRepository.kt index b91ffa5..3587baa 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreRepository.kt @@ -1,9 +1,24 @@ package kr.co.vividnext.sodalive.admin.content.series.genre +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.admin.content.series.genre.QSeriesGenre.seriesGenre import org.springframework.data.jpa.repository.JpaRepository interface AdminContentSeriesGenreRepository : JpaRepository, AdminContentSeriesGenreQueryRepository -interface AdminContentSeriesGenreQueryRepository +interface AdminContentSeriesGenreQueryRepository { + fun getSeriesGenreList(): List +} -class AdminContentSeriesGenreQueryRepositoryImpl : AdminContentSeriesGenreQueryRepository +class AdminContentSeriesGenreQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : AdminContentSeriesGenreQueryRepository { + override fun getSeriesGenreList(): List { + return queryFactory + .select(QGetSeriesGenreListResponse(seriesGenre.id, seriesGenre.genre, seriesGenre.isAdult)) + .from(seriesGenre) + .where(seriesGenre.isActive.isTrue) + .orderBy(seriesGenre.orders.asc()) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreService.kt index 058b782..68eb284 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/AdminContentSeriesGenreService.kt @@ -9,8 +9,8 @@ import org.springframework.transaction.annotation.Transactional class AdminContentSeriesGenreService(private val repository: AdminContentSeriesGenreRepository) { @Transactional - fun createSeriesGenre(genre: String) { - val seriesGenre = SeriesGenre(genre = genre.trim()) + fun createSeriesGenre(genre: String, isAdult: Boolean) { + val seriesGenre = SeriesGenre(genre = genre.trim(), isAdult = isAdult) repository.save(seriesGenre) } @@ -33,6 +33,10 @@ class AdminContentSeriesGenreService(private val repository: AdminContentSeriesG if (request.isActive != null) { seriesGenre.isActive = request.isActive + + if (!request.isActive) { + seriesGenre.genre = "${seriesGenre.genre}_deleted" + } } } @@ -46,4 +50,8 @@ class AdminContentSeriesGenreService(private val repository: AdminContentSeriesG } } } + + fun getSeriesGenreList(): List { + return repository.getSeriesGenreList() + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/GetSeriesGenreListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/GetSeriesGenreListResponse.kt new file mode 100644 index 0000000..d642c47 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/admin/content/series/genre/GetSeriesGenreListResponse.kt @@ -0,0 +1,9 @@ +package kr.co.vividnext.sodalive.admin.content.series.genre + +import com.querydsl.core.annotations.QueryProjection + +data class GetSeriesGenreListResponse @QueryProjection constructor( + val id: Long, + val genre: String, + val isAdult: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt index 31e9310..c95e582 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/can/charge/ChargeService.kt @@ -258,7 +258,7 @@ class ChargeService( private fun consumeWithRetry(productId: String, purchaseToken: String, charge: Charge, member: Member): Boolean { var attempt = 0 var delay = 2000L - val retries = 3 + val retries = 5 var lastError: Exception? = null diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreateSeriesRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreateSeriesRequest.kt new file mode 100644 index 0000000..658c1cc --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreateSeriesRequest.kt @@ -0,0 +1,38 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import kr.co.vividnext.sodalive.common.SodaException + +data class CreateSeriesRequest( + val title: String, + val introduction: String, + val publishedDaysOfWeek: Set, + val keyword: String, + val genreId: Long = 0, + val isAdult: Boolean = false, + val writer: String? = null, + val studio: String? = null +) { + fun toSeries(): Series { + validate() + + return Series( + title = title, + introduction = introduction, + writer = writer, + studio = studio, + publishedDaysOfWeek = publishedDaysOfWeek, + isAdult = isAdult + ) + } + + private fun validate() { + if (title.isBlank()) throw SodaException("시리즈 제목을 입력하세요") + if (introduction.isBlank()) throw SodaException("시리즈 소개를 입력하세요") + if (keyword.isBlank()) throw SodaException("시리즈를 설명할 수 있는 키워드를 입력하세요") + if (genreId <= 0) throw SodaException("올바른 장르를 선택하세요") + if (publishedDaysOfWeek.isEmpty()) throw SodaException("시리즈 연재요일을 선택하세요") + if (publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM) && publishedDaysOfWeek.size > 1) { + throw SodaException("랜덤과 연재요일 동시에 선택할 수 없습니다.") + } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesController.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesController.kt new file mode 100644 index 0000000..ce1adfd --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesController.kt @@ -0,0 +1,119 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import kr.co.vividnext.sodalive.common.ApiResponse +import kr.co.vividnext.sodalive.common.SodaException +import kr.co.vividnext.sodalive.creator.admin.content.series.content.AddingContentToTheSeriesRequest +import kr.co.vividnext.sodalive.creator.admin.content.series.content.RemoveContentToTheSeriesRequest +import kr.co.vividnext.sodalive.member.Member +import org.springframework.data.domain.Pageable +import org.springframework.lang.Nullable +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.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 +@PreAuthorize("hasRole('CREATOR')") +@RequestMapping("/creator-admin/audio-content/series") +class CreatorAdminContentSeriesController(private val service: CreatorAdminContentSeriesService) { + @PostMapping + fun createSeries( + @RequestPart("image") image: MultipartFile?, + @RequestPart("request") requestString: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.createSeries(image, requestString, member), "시리즈가 생성되었습니다.") + } + + @PutMapping + fun modifySeries( + @Nullable + @RequestPart("image") + image: MultipartFile?, + @RequestPart("request") requestString: String, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.modifySeries(image, requestString, member), "시리즈가 수정되었습니다.") + } + + @GetMapping + fun getSeriesList( + pageable: Pageable, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.getSeriesList( + offset = pageable.offset, + limit = pageable.pageSize.toLong(), + creatorId = member.id!! + ) + ) + } + + @GetMapping("/{id}") + fun getDetail( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok(service.getDetail(id = id, memberId = member.id!!)) + } + + @GetMapping("/{id}/content") + fun getSeriesContent( + @PathVariable id: Long, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member?, + pageable: Pageable + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.getSeriesContent( + seriesId = id, + offset = pageable.offset, + limit = pageable.pageSize.toLong(), + creatorId = member.id!! + ) + ) + } + + @PostMapping("/add/content") + fun addingContentToTheSeries( + @RequestBody request: AddingContentToTheSeriesRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.addingContentToTheSeries(request, memberId = member.id!!), + "콘텐츠가 추가되었습니다." + ) + } + + @PutMapping("/remove/content") + fun removeContentInTheSeries( + @RequestBody request: RemoveContentToTheSeriesRequest, + @AuthenticationPrincipal(expression = "#this == 'anonymousUser' ? null : member") member: Member? + ) = run { + if (member == null) throw SodaException("로그인 정보를 확인해주세요.") + + ApiResponse.ok( + service.removeContentInTheSeries(request, memberId = member.id!!), + "콘텐츠를 삭제하였습니다." + ) + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesRepository.kt new file mode 100644 index 0000000..14fd766 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesRepository.kt @@ -0,0 +1,122 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import com.querydsl.jpa.impl.JPAQueryFactory +import kr.co.vividnext.sodalive.content.QAudioContent.audioContent +import kr.co.vividnext.sodalive.creator.admin.content.series.QSeries.series +import kr.co.vividnext.sodalive.creator.admin.content.series.QSeriesContent.seriesContent +import org.springframework.data.jpa.repository.JpaRepository + +interface CreatorAdminContentSeriesRepository : JpaRepository, CreatorAdminContentSeriesQueryRepository + +interface CreatorAdminContentSeriesQueryRepository { + fun findByIdAndCreatorId(id: Long, creatorId: Long): Series? + fun getSeriesCount(creatorId: Long): Int + fun getSeriesList( + offset: Long, + limit: Long, + creatorId: Long, + imageHost: String + ): List + + fun getSeriesContentCount(creatorId: Long): Int + fun getSeriesContentList( + offset: Long, + limit: Long, + creatorId: Long, + imageHost: String + ): List +} + +class CreatorAdminContentSeriesQueryRepositoryImpl( + private val queryFactory: JPAQueryFactory +) : CreatorAdminContentSeriesQueryRepository { + override fun findByIdAndCreatorId(id: Long, creatorId: Long): Series? { + return queryFactory + .selectFrom(series) + .where( + series.id.eq(id) + .and(series.member.id.eq(creatorId)) + ) + .fetchFirst() + } + + override fun getSeriesCount(creatorId: Long): Int { + return queryFactory + .select(series.id) + .from(series) + .where( + series.member.id.eq(creatorId) + .and(series.isActive.isTrue) + ) + .fetch() + .size + } + + override fun getSeriesList( + offset: Long, + limit: Long, + creatorId: Long, + imageHost: String + ): List { + return queryFactory + .select( + QGetCreatorAdminContentSeriesListItem( + series.id, + series.title, + series.coverImage.prepend("/").prepend(imageHost) + ) + ) + .from(series) + .where( + series.member.id.eq(creatorId) + .and(series.isActive.isTrue) + ) + .orderBy(series.id.desc()) + .offset(offset) + .limit(limit) + .fetch() + } + + override fun getSeriesContentCount(creatorId: Long): Int { + return queryFactory + .select(seriesContent.id) + .from(seriesContent) + .innerJoin(seriesContent.series, series) + .innerJoin(seriesContent.content, audioContent) + .where( + series.member.id.eq(creatorId) + .and(audioContent.member.id.eq(creatorId)) + .and(series.isActive.isTrue) + ) + .fetch() + .size + } + + override fun getSeriesContentList( + offset: Long, + limit: Long, + creatorId: Long, + imageHost: String + ): List { + return queryFactory + .select( + QGetCreatorAdminContentSeriesContentItem( + audioContent.id, + audioContent.coverImage.prepend("/").prepend(imageHost), + audioContent.title, + audioContent.isAdult + ) + ) + .from(seriesContent) + .innerJoin(seriesContent.series, series) + .innerJoin(seriesContent.content, audioContent) + .where( + series.member.id.eq(creatorId) + .and(audioContent.member.id.eq(creatorId)) + .and(series.isActive.isTrue) + ) + .offset(offset) + .limit(limit) + .fetch() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesService.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesService.kt new file mode 100644 index 0000000..d000fe4 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/CreatorAdminContentSeriesService.kt @@ -0,0 +1,234 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import com.amazonaws.services.s3.model.ObjectMetadata +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.AudioContentRepository +import kr.co.vividnext.sodalive.content.hashtag.HashTag +import kr.co.vividnext.sodalive.content.hashtag.HashTagRepository +import kr.co.vividnext.sodalive.creator.admin.content.series.content.AddingContentToTheSeriesRequest +import kr.co.vividnext.sodalive.creator.admin.content.series.content.RemoveContentToTheSeriesRequest +import kr.co.vividnext.sodalive.creator.admin.content.series.genre.CreatorAdminContentSeriesGenreRepository +import kr.co.vividnext.sodalive.creator.admin.content.series.keyword.SeriesKeyword +import kr.co.vividnext.sodalive.member.Member +import kr.co.vividnext.sodalive.utils.generateFileName +import org.springframework.beans.factory.annotation.Value +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional +import org.springframework.web.multipart.MultipartFile + +@Service +class CreatorAdminContentSeriesService( + private val repository: CreatorAdminContentSeriesRepository, + private val genreRepository: CreatorAdminContentSeriesGenreRepository, + private val hashTagRepository: HashTagRepository, + private val audioContentRepository: AudioContentRepository, + + private val s3Uploader: S3Uploader, + private val objectMapper: ObjectMapper, + + @Value("\${cloud.aws.s3.bucket}") + private val coverImageBucket: String, + + @Value("\${cloud.aws.cloud-front.host}") + private val coverImageHost: String +) { + @Transactional + fun createSeries(coverImage: MultipartFile?, requestString: String, member: Member) { + if (coverImage == null) throw SodaException("커버이미지를 선택해 주세요.") + + val request = objectMapper.readValue(requestString, CreateSeriesRequest::class.java) + val series = request.toSeries() + val genre = genreRepository.findActiveSeriesGenreById(request.genreId) + series.genre = genre + series.member = member + repository.save(series) + + val keywords = request.keyword + .replace("#", " #") + .split(" ") + .map { it.trim() } + .filter { it.isNotBlank() } + .map { + val tag = if (!it.startsWith("#")) { + "#$it" + } else { + it + } + + val hashTag = hashTagRepository.findByTag(tag) + ?: hashTagRepository.save(HashTag(tag)) + + val seriesKeyword = SeriesKeyword() + seriesKeyword.series = series + seriesKeyword.keyword = hashTag + + seriesKeyword + } + + series.keywordList.addAll(keywords) + + val metadata = ObjectMetadata() + metadata.contentLength = coverImage.size + + // 커버 이미지 파일명 생성 + val coverImageFileName = generateFileName(prefix = "${series.id}-cover") + // 커버 이미지 업로드 + val coverImagePath = s3Uploader.upload( + inputStream = coverImage.inputStream, + bucket = coverImageBucket, + filePath = "series_cover/${series.id}/$coverImageFileName", + metadata = metadata + ) + + series.coverImage = coverImagePath + } + + @Transactional + fun modifySeries(coverImage: MultipartFile?, requestString: String, member: Member) { + val request = objectMapper.readValue(requestString, ModifySeriesRequest::class.java) + + if ( + coverImage == null && + request.title == null && + request.introduction == null && + request.publishedDaysOfWeek == null && + request.genreId == null && + request.isAdult == null && + request.writer == null && + request.studio == null + ) { + throw SodaException("변경사항이 없습니다.") + } + + val series = repository.findByIdAndCreatorId(id = request.seriesId, creatorId = member.id!!) + ?: throw SodaException("잘못된 접근입니다.") + + if (coverImage != null) { + val metadata = ObjectMetadata() + metadata.contentLength = coverImage.size + + // 커버 이미지 파일명 생성 + val coverImageFileName = generateFileName(prefix = "${series.id}-cover") + // 커버 이미지 업로드 + val coverImagePath = s3Uploader.upload( + inputStream = coverImage.inputStream, + bucket = coverImageBucket, + filePath = "series_cover/${series.id}/$coverImageFileName", + metadata = metadata + ) + + series.coverImage = coverImagePath + } + + if (request.title != null) { + series.title = request.title + } + + if (request.introduction != null) { + series.introduction = request.introduction + } + + if (request.publishedDaysOfWeek != null) { + if ( + request.publishedDaysOfWeek.contains(SeriesPublishedDaysOfWeek.RANDOM) && + request.publishedDaysOfWeek.size > 1 + ) { + throw SodaException("랜덤과 연재요일 동시에 선택할 수 없습니다.") + } + + series.publishedDaysOfWeek.toMutableSet().clear() + series.publishedDaysOfWeek.toMutableSet().addAll(request.publishedDaysOfWeek) + } + + if (request.genreId != null) { + val genre = genreRepository.findActiveSeriesGenreById(request.genreId) + series.genre = genre + } + + if (request.isAdult != null) { + series.isAdult = request.isAdult + } + + if (request.isActive != null) { + series.isActive = request.isActive + } + + if (request.writer != null) { + series.writer = request.writer + } + + if (request.studio != null) { + series.studio = request.studio + } + } + + fun getSeriesList(offset: Long, limit: Long, creatorId: Long): GetCreatorAdminContentSeriesListResponse { + val totalCount = repository.getSeriesCount(creatorId = creatorId) + val seriesList = repository.getSeriesList( + offset = offset, + limit = limit, + creatorId = creatorId, + imageHost = coverImageHost + ) + + return GetCreatorAdminContentSeriesListResponse(totalCount, seriesList) + } + + fun getDetail(id: Long, memberId: Long): GetCreatorAdminContentSeriesDetailResponse { + val series = repository.findByIdAndCreatorId(id = id, creatorId = memberId) + ?: throw SodaException("잘못된 접근입니다.") + + return series.toDetailResponse(imageHost = coverImageHost) + } + + fun getSeriesContent( + seriesId: Long, + offset: Long, + limit: Long, + creatorId: Long + ): GetCreatorAdminContentSeriesContentResponse { + val totalCount = repository.getSeriesContentCount(creatorId = creatorId) + val seriesContentList = repository.getSeriesContentList( + offset = offset, + limit = limit, + creatorId = creatorId, + imageHost = coverImageHost + ) + + return GetCreatorAdminContentSeriesContentResponse(totalCount, seriesContentList) + } + + @Transactional + fun addingContentToTheSeries(request: AddingContentToTheSeriesRequest, memberId: Long) { + val series = repository.findByIdAndCreatorId(id = request.seriesId, creatorId = memberId) + ?: throw SodaException("잘못된 접근입니다.") + + val seriesContentList = mutableListOf() + + for (contentId in request.contentIdList) { + val content = audioContentRepository.findByIdAndCreatorId(contentId = contentId, creatorId = memberId) + ?: continue + + val seriesContent = SeriesContent() + seriesContent.series = series + seriesContent.content = content + seriesContentList.add(seriesContent) + } + + if (seriesContentList.size > 0) { + series.contentList.addAll(seriesContentList) + } else { + throw SodaException("추가된 콘텐츠가 없습니다.") + } + } + + @Transactional + fun removeContentInTheSeries(request: RemoveContentToTheSeriesRequest, memberId: Long) { + val series = repository.findByIdAndCreatorId(id = request.seriesId, creatorId = memberId) + ?: throw SodaException("잘못된 접근입니다.") + + series.contentList.removeIf { it.content!!.id == request.contentId } + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesContentResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesContentResponse.kt new file mode 100644 index 0000000..fd9118f --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesContentResponse.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import com.querydsl.core.annotations.QueryProjection + +data class GetCreatorAdminContentSeriesContentResponse( + val totalCount: Int, + val items: List +) + +data class GetCreatorAdminContentSeriesContentItem @QueryProjection constructor( + val contentId: Long, + val coverImage: String, + val title: String, + val isAdult: Boolean +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesDetailResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesDetailResponse.kt new file mode 100644 index 0000000..9cc2271 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesDetailResponse.kt @@ -0,0 +1,15 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +data class GetCreatorAdminContentSeriesDetailResponse( + val seriesId: Long, + val title: String, + val introduction: String, + val coverImage: String, + val publishedDaysOfWeek: String, + val genre: String, + val keywords: String, + val isAdult: Boolean, + val state: String, + val writer: String?, + val studio: String? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesListResponse.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesListResponse.kt new file mode 100644 index 0000000..da32444 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/GetCreatorAdminContentSeriesListResponse.kt @@ -0,0 +1,14 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import com.querydsl.core.annotations.QueryProjection + +data class GetCreatorAdminContentSeriesListResponse( + val totalCount: Int, + val items: List +) + +data class GetCreatorAdminContentSeriesListItem @QueryProjection constructor( + val seriesId: Long, + val title: String, + val coverImageUrl: String +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/ModifySeriesRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/ModifySeriesRequest.kt new file mode 100644 index 0000000..4057c55 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/ModifySeriesRequest.kt @@ -0,0 +1,13 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +data class ModifySeriesRequest( + val seriesId: Long, + val title: String?, + val introduction: String?, + val publishedDaysOfWeek: Set?, + val genreId: Long?, + val isAdult: Boolean?, + val isActive: Boolean?, + val writer: String?, + val studio: String? +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt index b8c7521..c801f13 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/Series.kt @@ -2,6 +2,9 @@ package kr.co.vividnext.sodalive.creator.admin.content.series import kr.co.vividnext.sodalive.admin.content.series.genre.SeriesGenre import kr.co.vividnext.sodalive.common.BaseEntity +import kr.co.vividnext.sodalive.creator.admin.content.series.keyword.SeriesKeyword +import kr.co.vividnext.sodalive.member.Member +import javax.persistence.CascadeType import javax.persistence.CollectionTable import javax.persistence.Column import javax.persistence.ElementCollection @@ -10,6 +13,8 @@ import javax.persistence.EnumType import javax.persistence.Enumerated import javax.persistence.FetchType import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.OneToMany import javax.persistence.OneToOne enum class SeriesPublishedDaysOfWeek { @@ -27,6 +32,8 @@ data class Series( var introduction: String, @Enumerated(value = EnumType.STRING) var state: SeriesState = SeriesState.PROCEEDING, + var writer: String? = null, + var studio: String? = null, @ElementCollection(targetClass = SeriesPublishedDaysOfWeek::class, fetch = FetchType.EAGER) @Enumerated(value = EnumType.STRING) @CollectionTable(name = "series_published_days_of_week", joinColumns = [JoinColumn(name = "series_id")]) @@ -38,5 +45,54 @@ data class Series( @JoinColumn(name = "genre_id", nullable = false) var genre: SeriesGenre? = null + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "member_id", nullable = false) + var member: Member? = null + var coverImage: String? = null + + @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL]) + var contentList: MutableList = mutableListOf() + + @OneToMany(mappedBy = "series", cascade = [CascadeType.ALL]) + var keywordList: MutableList = mutableListOf() + + fun toDetailResponse(imageHost: String): GetCreatorAdminContentSeriesDetailResponse { + return GetCreatorAdminContentSeriesDetailResponse( + seriesId = id!!, + title = title, + introduction = introduction, + coverImage = "$imageHost/$coverImage!!", + publishedDaysOfWeek = publishedDaysOfWeekText(), + genre = genre!!.genre, + keywords = keywordList.map { it.keyword!!.tag }.joinToString(" ") { it }, + isAdult = isAdult, + state = stateSeriesText(), + writer = writer, + studio = studio + ) + } + + private fun publishedDaysOfWeekText(): String { + return publishedDaysOfWeek.toList().sortedBy { it.ordinal }.map { + when (it) { + SeriesPublishedDaysOfWeek.SUN -> "일" + SeriesPublishedDaysOfWeek.MON -> "월" + SeriesPublishedDaysOfWeek.TUE -> "화" + SeriesPublishedDaysOfWeek.WED -> "수" + SeriesPublishedDaysOfWeek.THU -> "목" + SeriesPublishedDaysOfWeek.FRI -> "금" + SeriesPublishedDaysOfWeek.SAT -> "토" + SeriesPublishedDaysOfWeek.RANDOM -> "랜덤" + } + }.joinToString(", ") { it } + } + + private fun stateSeriesText(): String { + return when (state) { + SeriesState.PROCEEDING -> "연재중" + SeriesState.SUSPEND -> "휴재중" + SeriesState.COMPLETE -> "완결" + } + } } diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/SeriesContent.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/SeriesContent.kt new file mode 100644 index 0000000..eabc927 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/SeriesContent.kt @@ -0,0 +1,34 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series + +import kr.co.vividnext.sodalive.content.AudioContent +import java.time.LocalDateTime +import javax.persistence.Entity +import javax.persistence.FetchType +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id +import javax.persistence.JoinColumn +import javax.persistence.ManyToOne +import javax.persistence.PrePersist + +@Entity +class SeriesContent { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + + var createdAt: LocalDateTime? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "series_id", nullable = false) + var series: Series? = null + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "content_id", nullable = false) + var content: AudioContent? = null + + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + } +} diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/content/AddingContentToTheSeriesRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/content/AddingContentToTheSeriesRequest.kt new file mode 100644 index 0000000..b9a9525 --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/content/AddingContentToTheSeriesRequest.kt @@ -0,0 +1,6 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series.content + +data class AddingContentToTheSeriesRequest( + val seriesId: Long, + val contentIdList: List +) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/content/RemoveContentToTheSeriesRequest.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/content/RemoveContentToTheSeriesRequest.kt new file mode 100644 index 0000000..42d19dc --- /dev/null +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/content/RemoveContentToTheSeriesRequest.kt @@ -0,0 +1,3 @@ +package kr.co.vividnext.sodalive.creator.admin.content.series.content + +data class RemoveContentToTheSeriesRequest(val seriesId: Long, val contentId: Long) diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/genre/CreatorAdminContentSeriesGenreRepository.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/genre/CreatorAdminContentSeriesGenreRepository.kt index e35112d..78fcc34 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/genre/CreatorAdminContentSeriesGenreRepository.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/genre/CreatorAdminContentSeriesGenreRepository.kt @@ -9,12 +9,23 @@ interface CreatorAdminContentSeriesGenreRepository : JpaRepository, CreatorAdminContentSeriesGenreQueryRepository interface CreatorAdminContentSeriesGenreQueryRepository { + fun findActiveSeriesGenreById(id: Long): SeriesGenre fun getGenreList(isAdult: Boolean): List } class CreatorAdminContentSeriesGenreQueryRepositoryImpl( private val queryFactory: JPAQueryFactory ) : CreatorAdminContentSeriesGenreQueryRepository { + override fun findActiveSeriesGenreById(id: Long): SeriesGenre { + return queryFactory + .selectFrom(seriesGenre) + .where( + seriesGenre.id.eq(id) + .and(seriesGenre.isActive.isTrue) + ) + .fetchFirst() + } + override fun getGenreList(isAdult: Boolean): List { var where = seriesGenre.isActive.isTrue diff --git a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/keyword/SeriesKeyword.kt b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/keyword/SeriesKeyword.kt index 32d90dd..d36e591 100644 --- a/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/keyword/SeriesKeyword.kt +++ b/src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/keyword/SeriesKeyword.kt @@ -1,15 +1,25 @@ package kr.co.vividnext.sodalive.creator.admin.content.series.keyword -import kr.co.vividnext.sodalive.common.BaseEntity import kr.co.vividnext.sodalive.content.hashtag.HashTag import kr.co.vividnext.sodalive.creator.admin.content.series.Series +import java.time.LocalDateTime import javax.persistence.Entity import javax.persistence.FetchType +import javax.persistence.GeneratedValue +import javax.persistence.GenerationType +import javax.persistence.Id import javax.persistence.JoinColumn import javax.persistence.ManyToOne +import javax.persistence.PrePersist @Entity -class SeriesKeyword : BaseEntity() { +class SeriesKeyword { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + var id: Long? = null + + var createdAt: LocalDateTime? = null + @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "series_id", nullable = false) var series: Series? = null @@ -18,5 +28,8 @@ class SeriesKeyword : BaseEntity() { @JoinColumn(name = "keyword_id", nullable = false) var keyword: HashTag? = null - var isActive: Boolean = true + @PrePersist + fun prePersist() { + createdAt = LocalDateTime.now() + } }