Skip to content

Commit

Permalink
[feat #33] 컨텐츠 조회 API(목록, 상세) (#38)
Browse files Browse the repository at this point in the history
* chore : QueryDsl 설정 추가

* feat : 엔티티 필드 수정

* feat : UserLog 정의

* feat : 컨텐츠 조회 영속성 계층

* feat : 컨텐츠 조회 어플리케이션 계층

* feat : 컨텐츠 조회 API

* feat : 컨텐츠 조회 dto

* chore : out-persistence 테스트용 yml 파일

* feat : UserLog 저장 방식 수정

* rename : yml local 프로파일
  • Loading branch information
dlswns2480 authored Jul 29, 2024
1 parent 3898214 commit d4c6d7c
Show file tree
Hide file tree
Showing 23 changed files with 431 additions and 9 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,9 @@ import com.pokit.auth.config.ErrorOperation
import com.pokit.auth.model.PrincipalUser
import com.pokit.auth.model.toDomain
import com.pokit.category.exception.CategoryErrorCode
import com.pokit.common.dto.SliceResponseDto
import com.pokit.common.wrapper.ResponseWrapper.wrapOk
import com.pokit.common.wrapper.ResponseWrapper.wrapSlice
import com.pokit.common.wrapper.ResponseWrapper.wrapUnit
import com.pokit.content.dto.request.CreateContentRequest
import com.pokit.content.dto.request.UpdateContentRequest
Expand All @@ -13,9 +15,13 @@ import com.pokit.content.dto.response.BookMarkContentResponse
import com.pokit.content.dto.response.ContentResponse
import com.pokit.content.dto.response.toResponse
import com.pokit.content.exception.ContentErrorCode
import com.pokit.content.model.Content
import com.pokit.content.port.`in`.ContentUseCase
import io.swagger.v3.oas.annotations.Operation
import jakarta.validation.Valid
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
import org.springframework.data.web.PageableDefault
import org.springframework.http.ResponseEntity
import org.springframework.security.core.annotation.AuthenticationPrincipal
import org.springframework.web.bind.annotation.*
Expand Down Expand Up @@ -87,4 +93,35 @@ class ContentController(
return contentUseCase.cancelBookmark(user, contentId)
.wrapUnit()
}

@GetMapping("/{categoryId}")
@Operation(summary = "카테고리 내 컨텐츠 목록 조회")
fun getContents(
@AuthenticationPrincipal user: PrincipalUser,
@PathVariable("categoryId") contentId: Long,
@PageableDefault(
page = 0,
size = 10,
sort = ["createdAt"],
direction = Sort.Direction.DESC
) pageable: Pageable,
@RequestParam(required = false) isRead: Boolean?,
@RequestParam(required = false) favorites: Boolean?
): ResponseEntity<SliceResponseDto<Content>>? {
return contentUseCase.getContents(user.id, contentId, pageable, isRead, favorites)
.wrapSlice()
.wrapOk()
}

@PostMapping("/{contentId}")
@Operation(summary = "컨텐츠 상세조회 API")
fun getContent(
@AuthenticationPrincipal user: PrincipalUser,
@PathVariable("contentId") contentId: Long
): ResponseEntity<ContentResponse> {
return contentUseCase.getContent(user.id, contentId)
.toResponse()
.wrapOk()
}
}

Original file line number Diff line number Diff line change
@@ -1,19 +1,37 @@
package com.pokit.content.dto.response

import com.fasterxml.jackson.annotation.JsonFormat
import com.pokit.content.model.Content

data class ContentResponse(
val contentId: Long,
val categoryId: Long,
val data: String,
val title: String,
val memo: String,
val alertYn: String
val alertYn: String,
@JsonFormat(pattern = "yyyy-MM-dd HH:mm:")
val createdAt: String,
val favorites: Boolean = false
)

fun Content.toResponse() = ContentResponse(
contentId = this.id,
categoryId = this.categoryId,
data = this.data,
title = this.title,
memo = this.memo,
alertYn = this.alertYn
alertYn = this.alertYn,
createdAt = this.createdAt.toString()
)

fun GetContentResponse.toResponse() = ContentResponse(
contentId = this.contentId,
categoryId = this.categoryId,
data = this.data,
title = this.title,
memo = this.memo,
alertYn = this.alertYn,
createdAt = this.createdAt.toString(),
favorites = this.favorites
)
28 changes: 28 additions & 0 deletions adapters/out-persistence/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,12 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
runtimeOnly("com.mysql:mysql-connector-j")

// QueryDSL
implementation("com.querydsl:querydsl-jpa:5.0.0:jakarta")
kapt("com.querydsl:querydsl-apt:5.0.0:jakarta")
kapt("jakarta.annotation:jakarta.annotation-api")
kapt("jakarta.persistence:jakarta.persistence-api")

// 테스팅
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.1")

Expand All @@ -40,3 +46,25 @@ tasks {
withType<Jar> { enabled = true }
withType<BootJar> { enabled = false }
}

val generated = file("src/main/generated")

tasks.withType<JavaCompile> {
options.generatedSourceOutputDirectory.set(generated)
}

sourceSets {
main {
kotlin.srcDirs += generated
}
}

tasks.named("clean") {
doLast {
generated.deleteRecursively()
}
}

kapt {
generateStubs = true
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,4 +24,12 @@ class BookMarkAdapter(
false
)?.delete()
}

override fun loadByContentIdAndUserId(contentId: Long, userId: Long): Bookmark? {
return bookMarkRepository.findByContentIdAndUserIdAndDeleted(
contentId,
userId,
false
)?.toDomain()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ class BookmarkEntity(
val userId: Long,

@Column(name = "deleted")
var deleted: Boolean = true
var deleted: Boolean = false
) {
fun delete() {
this.deleted = true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ class CategoryEntity(
var name: String,

@OneToOne
@JoinColumn(name = "image_id")
@JoinColumn(name = "image_id", foreignKey = ForeignKey(value = ConstraintMode.NO_CONSTRAINT))
val image: CategoryImageEntity,
) : BaseEntity() {

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.pokit.out.persistence.config

import com.querydsl.jpa.impl.JPAQueryFactory
import jakarta.persistence.EntityManager
import jakarta.persistence.PersistenceContext
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration

@Configuration
class QueryDslConfig {
@PersistenceContext
lateinit var entityManager: EntityManager

@Bean
fun queryFactory() = JPAQueryFactory(entityManager)
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,30 @@ package com.pokit.out.persistence.content.impl

import com.pokit.content.model.Content
import com.pokit.content.port.out.ContentPort
import com.pokit.log.model.LogType
import com.pokit.out.persistence.bookmark.persist.QBookmarkEntity.bookmarkEntity
import com.pokit.out.persistence.category.persist.QCategoryEntity.categoryEntity
import com.pokit.out.persistence.content.persist.ContentEntity
import com.pokit.out.persistence.content.persist.ContentRepository
import com.pokit.out.persistence.content.persist.QContentEntity.contentEntity
import com.pokit.out.persistence.content.persist.toDomain
import com.pokit.out.persistence.log.persist.QUserLogEntity.userLogEntity
import com.pokit.out.persistence.user.persist.QUserEntity.userEntity
import com.querydsl.core.types.dsl.DateTimePath
import com.querydsl.jpa.impl.JPAQuery
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice
import org.springframework.data.domain.SliceImpl
import org.springframework.data.domain.Sort
import org.springframework.data.repository.findByIdOrNull
import org.springframework.stereotype.Repository
import java.time.LocalDateTime

@Repository
class ContentAdapter(
private val contentRepository: ContentRepository
private val contentRepository: ContentRepository,
private val queryFactory: JPAQueryFactory
) : ContentPort {
override fun loadByUserIdAndId(userId: Long, id: Long) = contentRepository.findByUserIdAndIdAndDeleted(userId, id)
?.run { toDomain() }
Expand All @@ -28,4 +43,74 @@ class ContentAdapter(
override fun fetchContentCountByCategoryId(categoryId: Long): Int =
contentRepository.countByCategoryId(categoryId)


override fun loadAllByUserIdAndContentId(
userId: Long,
categoryId: Long,
pageable: Pageable,
read: Boolean?,
favorites: Boolean?
): Slice<Content> {
var hasNext = false
val order = pageable.sort.getOrderFor("createdAt")

val query = queryFactory.select(contentEntity)
.from(contentEntity)
.join(categoryEntity).on(categoryEntity.id.eq(contentEntity.categoryId))
.join(userEntity).on(userEntity.id.eq(categoryEntity.userId))

FavoriteOrNot(favorites, query) // 북마크 조인 여부
ReadOrNot(read, query) // 읽음 로그 조인 여부

query.where(
userEntity.id.eq(userId),
categoryEntity.id.eq(categoryId),
contentEntity.deleted.isFalse
)
.orderBy(getSort(contentEntity.createdAt, order!!))
.limit(pageable.pageSize + 1L)


val contentEntityList = query.fetch()

if (contentEntityList.size > pageable.pageSize) {
hasNext = true
contentEntityList.removeAt(contentEntityList.size - 1)
}

val contents = contentEntityList.map { it.toDomain() }

return SliceImpl(contents, pageable, hasNext)
}

private fun ReadOrNot(
read: Boolean?,
query: JPAQuery<ContentEntity>
): JPAQuery<ContentEntity>? {
return read
?.let {
query
.leftJoin(userLogEntity)
.on(userLogEntity.contentId.eq(contentEntity.id))
.where(userLogEntity.id.isNull.or(userLogEntity.type.ne(LogType.READ)))
}
}

private fun FavoriteOrNot(
favorites: Boolean?,
query: JPAQuery<ContentEntity>
): JPAQuery<ContentEntity>? {
return favorites
?.let {
query
.join(bookmarkEntity)
.on(bookmarkEntity.contentId.eq(contentEntity.id)
.and(bookmarkEntity.deleted.isFalse))
}
}

private fun getSort(property: DateTimePath<LocalDateTime>, order: Sort.Order) =
if (order.isDescending) property.desc()
else property.asc()

}
Original file line number Diff line number Diff line change
Expand Up @@ -60,4 +60,5 @@ fun ContentEntity.toDomain() = Content(
title = this.title,
memo = this.memo,
alertYn = this.alertYn,
createdAt = this.createdAt
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.pokit.out.persistence.log

import com.pokit.log.model.UserLog
import com.pokit.log.port.out.UserLogPort
import com.pokit.out.persistence.log.persist.UserLogEntity
import com.pokit.out.persistence.log.persist.UserLogRepository
import com.pokit.out.persistence.log.persist.toDomain
import org.springframework.stereotype.Repository

@Repository
class UserLogAdapter(
private val userLogRepository: UserLogRepository
) : UserLogPort {
override fun persist(userLog: UserLog) =
userLogRepository.save(UserLogEntity.of(userLog)).toDomain()
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.pokit.out.persistence.log.persist

import com.pokit.log.model.LogType
import com.pokit.log.model.UserLog
import com.pokit.out.persistence.BaseEntity
import jakarta.persistence.*

Expand All @@ -21,4 +22,17 @@ class UserLogEntity(
@Enumerated(EnumType.STRING)
val type: LogType,
) : BaseEntity() {
companion object {
fun of(userLog: UserLog) = UserLogEntity(
contentId = userLog.contentId,
userId = userLog.contentId,
type = userLog.type
)
}
}

internal fun UserLogEntity.toDomain() = UserLog(
contentId = this.contentId,
userId = this.userId,
type = this.type
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package com.pokit.out.persistence.log.persist

import org.springframework.data.jpa.repository.JpaRepository

interface UserLogRepository : JpaRepository<UserLogEntity, Long> {
fun findByContentIdAndUserId(contentId: Long, userId: Long): UserLogEntity?
}
Loading

0 comments on commit d4c6d7c

Please sign in to comment.