Skip to content

Commit

Permalink
[feat #54] 오늘의 리마인드 api (#127)
Browse files Browse the repository at this point in the history
* feat: 필드명 추가

* feat: daily content 생성 및 삭제 로직 추가

* feat: 오늘의 리마인드 api 수정

* feat: 오늘의 리마인드 스케줄러 설정

* feat: 코드리뷰 반영

* feat: batch 서버 배포 테스트

* feat: 임시 설정

* feat: 랜덤 개수 선정 수정

* feat: 설정 원복

* chore: push branch 수정
  • Loading branch information
jimin3263 authored Aug 24, 2024
1 parent a9a2e0f commit a90b018
Show file tree
Hide file tree
Showing 24 changed files with 299 additions and 51 deletions.
1 change: 0 additions & 1 deletion .github/workflows/batch-deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ name: batch deploy
on:
push:
branches:
- feature/#58
- main
- develop
paths:
Expand Down
2 changes: 2 additions & 0 deletions adapters/in-batch/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,8 @@ dependencies {
implementation("org.springframework.boot:spring-boot-starter-web")
implementation("org.springframework.boot:spring-boot-starter-actuator")
implementation("org.springframework.boot:spring-boot-starter-batch")
implementation("org.springframework.boot:spring-boot-starter-quartz")
implementation("com.navercorp.spring:spring-boot-starter-batch-plus-kotlin:1.1.0")

// 테스팅
testImplementation("io.kotest.extensions:kotest-extensions-spring:1.1.1")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package com.pokit.remind.job

import com.navercorp.spring.batch.plus.kotlin.configuration.BatchDsl
import com.pokit.content.port.`in`.DailyContentUseCase
import com.pokit.user.port.`in`.UserUseCase
import org.springframework.batch.core.Job
import org.springframework.batch.core.Step
import org.springframework.batch.repeat.RepeatStatus
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration
import org.springframework.transaction.PlatformTransactionManager

@Configuration
class DailyContentConfig(
private val transactionManager: PlatformTransactionManager,
private val dailyContentUseCase: DailyContentUseCase,
private val userUseCase: UserUseCase,
private val batch: BatchDsl,
) {

@Bean
fun updateDailyContentJob(): Job = batch {
job("updateDailyContentJob") {
step(deleteAllStep()) {
on("COMPLETED") {
step(updateDailyContentStep())
}
}
}
}

@Bean
fun deleteAllStep(): Step = batch {
step("deleteAllStep") {
tasklet({ _, _ ->
dailyContentUseCase.deleteAll()
RepeatStatus.FINISHED
}, transactionManager)
}
}

@Bean
fun updateDailyContentStep(): Step = batch {
step("updateDailyContentStep") {
tasklet({ _, _ ->
val userIds: List<Long> = userUseCase.fetchAllUserId()

for (userId in userIds) {
dailyContentUseCase.registerDailyContent(userId)
}
RepeatStatus.FINISHED
}, transactionManager)
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.pokit.remind.scheduler

import io.github.oshai.kotlinlogging.KotlinLogging
import org.springframework.batch.core.Job
import org.springframework.batch.core.JobParametersBuilder
import org.springframework.batch.core.launch.JobLauncher
import org.springframework.scheduling.annotation.Scheduled
import org.springframework.stereotype.Component

@Component
class DailyContentUpdateScheduler(
private val jobLauncher: JobLauncher,
private val updateDailyContent: Job,
) {
private val logger = KotlinLogging.logger { }

companion object {
private const val 매일_자정 = "0 0 0 * * *"
}

@Scheduled(cron = 매일_자정)
fun updateDailyContent() {
val jobParameters = JobParametersBuilder()
.addLong("run.id", System.currentTimeMillis())
.toJobParameters()

logger.info { "[CONTENT BATCH] start daily content update job" }
jobLauncher.run(updateDailyContent, jobParameters)
logger.info { "[CONTENT BATCH] end daily content update job" }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import com.pokit.common.wrapper.ResponseWrapper.wrapSlice
import com.pokit.content.dto.response.RemindContentResponse
import com.pokit.content.dto.response.toResponse
import com.pokit.content.port.`in`.ContentUseCase
import com.pokit.content.port.`in`.DailyContentUseCase
import io.swagger.v3.oas.annotations.Operation
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Sort
Expand All @@ -20,7 +21,8 @@ import org.springframework.web.bind.annotation.RestController
@RestController
@RequestMapping("/api/v1/remind")
class RemindController(
private val contentUseCase: ContentUseCase
private val contentUseCase: ContentUseCase,
private val dailyContentUseCase: DailyContentUseCase,
) {
@GetMapping("/bookmark")
@Operation(summary = "즐겨찾기 링크 모음 조회 API")
Expand Down Expand Up @@ -56,10 +58,10 @@ class RemindController(

@GetMapping("/today")
@Operation(summary = "오늘의 리마인드 조회 API")
fun getUnreadContents(
fun getDailyContents(
@AuthenticationPrincipal user: PrincipalUser,
): ResponseEntity<List<RemindContentResponse>> =
contentUseCase.getTodayContents(user.id)
dailyContentUseCase.getDailyContents(user.id)
.map { it.toResponse() }
.wrapOk()

Expand Down
Original file line number Diff line number Diff line change
@@ -1,16 +1,18 @@
package com.pokit.content.dto.response

import com.fasterxml.jackson.annotation.JsonInclude
import com.pokit.category.model.RemindCategory
import java.time.format.DateTimeFormatter

@JsonInclude(JsonInclude.Include.NON_NULL)
data class RemindContentResponse(
val contentId: Long,
val category: RemindCategory,
val title: String,
val data: String,
val createdAt: String,
val domain: String,
val isRead: Boolean,
val isRead: Boolean?,
val thumbNail: String,
)

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,10 @@ import java.time.ZoneId
@EntityListeners(value = [AuditingEntityListener::class])
abstract class BaseEntity {
@CreatedDate
@Column(updatable = false)
@Column(updatable = false, name = "created_at")
var createdAt: LocalDateTime = LocalDateTime.now(ZoneId.of("Asia/Seoul"))

@LastModifiedDate
@Column(name = "updated_at")
var updatedAt: LocalDateTime = LocalDateTime.now(ZoneId.of("Asia/Seoul"))
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.pokit.out.persistence.content.impl

import com.pokit.content.dto.response.RemindContentResult
import com.pokit.content.port.out.DailyContentPort
import com.pokit.out.persistence.category.persist.QCategoryEntity.categoryEntity
import com.pokit.out.persistence.content.persist.DailyContentEntity
import com.pokit.out.persistence.content.persist.DailyContentRepository
import com.pokit.out.persistence.content.persist.QContentEntity.contentEntity
import com.pokit.out.persistence.content.persist.toDomain
import org.springframework.stereotype.Component

@Component
class DailyContentAdapter(
private val dailyContentRepository: DailyContentRepository,
) : DailyContentPort {
override fun fetchContentIdsByUserId(userId: Long) =
dailyContentRepository.getContentIdsByUserId(userId)

override fun loadByUserId(userId: Long): List<RemindContentResult> {
val contentEntityList = dailyContentRepository.getDailyContentsByUserId(userId)

return contentEntityList.map {
RemindContentResult.of(
it[contentEntity]!!.toDomain(),
it[categoryEntity.name]!!,
)
}
}

override fun persist(userId: Long, ids: List<Long>) {
val dailyContentEntities = ids.map { contentId ->
DailyContentEntity(
userId = userId,
contentId = contentId
)
}

dailyContentRepository.saveAll(dailyContentEntities)
}

override fun deleteAll() =
dailyContentRepository.deleteAll()

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.pokit.out.persistence.content.persist

import jakarta.persistence.*

@Table(name = "DAILY_CONTENT")
@Entity
class DailyContentEntity(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id")
val id: Long = 0L,

val userId: Long,

val contentId: Long,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.pokit.out.persistence.content.persist

import com.querydsl.core.Tuple

interface DailyContentQuerydslRepository {
fun getDailyContentsByUserId(userId: Long): List<Tuple>
fun getContentIdsByUserId(userId: Long): List<Long>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.pokit.out.persistence.content.persist

import com.pokit.out.persistence.category.persist.QCategoryEntity.categoryEntity
import com.pokit.out.persistence.content.persist.QContentEntity.contentEntity
import com.pokit.out.persistence.content.persist.QDailyContentEntity.dailyContentEntity
import com.querydsl.core.Tuple
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.stereotype.Repository

@Repository
class DailyContentQuerydslRepositoryImpl(
private val queryFactory: JPAQueryFactory
) : DailyContentQuerydslRepository {
override fun getDailyContentsByUserId(userId: Long): MutableList<Tuple> =
queryFactory.select(contentEntity, categoryEntity.name)
.from(contentEntity)
.join(dailyContentEntity).on(contentEntity.id.eq(dailyContentEntity.contentId))
.join(categoryEntity).on(contentEntity.categoryId.eq(categoryEntity.id))
.where(dailyContentEntity.userId.eq(userId))
.fetch()

override fun getContentIdsByUserId(userId: Long): List<Long> =
queryFactory
.select(contentEntity.id)
.from(contentEntity)
.join(categoryEntity).on(contentEntity.categoryId.eq(categoryEntity.id))
.where(
categoryEntity.userId.eq(userId)
.and(categoryEntity.deleted.eq(false))
.and(contentEntity.deleted.eq(false))
)
.fetch()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package com.pokit.out.persistence.content.persist

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

interface DailyContentRepository: JpaRepository<DailyContentEntity, Long>, DailyContentQuerydslRepository
Original file line number Diff line number Diff line change
Expand Up @@ -31,4 +31,9 @@ class UserAdapter(
userRepository.findByIdOrNull(user.id)
?.delete()
}

override fun loadAllIds(): List<Long> =
userRepository.findByDeleted(false)
.map { it.id }

}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@ interface UserRepository : JpaRepository<UserEntity, Long> {
fun existsByNickname(nickname: String): Boolean

fun findByIdAndDeleted(id: Long, deleted: Boolean): UserEntity?

fun findByDeleted(deleted: Boolean): List<UserEntity>
}
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,4 @@ interface ContentUseCase {
fun getBookmarkContents(userId: Long, pageable: Pageable): Slice<RemindContentResult>

fun getUnreadContents(userId: Long, pageable: Pageable): Slice<RemindContentResult>

fun getTodayContents(userId: Long): List<RemindContentResult>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package com.pokit.content.port.`in`

import com.pokit.content.dto.response.RemindContentResult

interface DailyContentUseCase {
fun registerDailyContent(userId: Long)
fun deleteAll()
fun getDailyContents(userId: Long): List<RemindContentResult>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.pokit.content.port.out

import com.pokit.content.dto.response.RemindContentResult

interface DailyContentPort {
fun loadByUserId(userId: Long): List<RemindContentResult>
fun fetchContentIdsByUserId(userId: Long): List<Long>
fun persist(userId: Long, ids: List<Long>)
fun deleteAll()
}
Original file line number Diff line number Diff line change
Expand Up @@ -136,46 +136,6 @@ class ContentService(
return SliceImpl(remindContents, pageable, unreadContents.hasNext())
}

override fun getTodayContents(userId: Long): List<RemindContentResult> {
// if (contentPort.countByUserId(userId) < MIN_CONTENT_COUNT) {
// return emptyList()
// }

//TODO 알고리즘 구현
return listOf(
RemindContentResult(
1L,
RemindCategory(1L, "category"),
"title",
"url",
LocalDateTime.now(),
"domain",
false,
"thumbNail"
),
RemindContentResult(
2L,
RemindCategory(1L, "category"),
"양궁 올림픽 덜덜",
"https://www.youtube.com/watch?v=s9L9yRL_I0s",
LocalDateTime.now(),
"youtu.be",
true,
"thumbNail"
),
RemindContentResult(
3L,
RemindCategory(1L, "category"),
"요리 맛있겠땅",
"https://www.youtube.com/shorts/aLZEwkm4tGU",
LocalDateTime.now(),
"youtu.be",
true,
"thumbNail"
),
)
}

private fun verifyContent(userId: Long, contentId: Long): Content {
return contentPort.loadByUserIdAndId(userId, contentId)
?: throw NotFoundCustomException(ContentErrorCode.NOT_FOUND_CONTENT)
Expand Down
Loading

0 comments on commit a90b018

Please sign in to comment.