Skip to content

Commit

Permalink
♻️ 커서 기반 페이징 방식 수정 (#222) (#227)
Browse files Browse the repository at this point in the history
* ♻️ 유실물 조회 origin 필드 제거 (#211)

* refactor: 커서 기반 페이징 방식 수정

* style: 불필요한 코드 제거

* refactor: 인덱스 수정

* refactor: 추천이 불가능한 유실물은 예외가 아니라 빈 리스트 반환

* refactor: DateTimeFormatter 공통 상수로 선언

* refactor: PageTokenUtil 확장성 있게 처리하도록 수정
  • Loading branch information
semi-cloud authored Oct 14, 2024
1 parent f38f07a commit b18f622
Show file tree
Hide file tree
Showing 16 changed files with 258 additions and 149 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ package backend.team.ahachul_backend.api.lost.adapter.web.`in`
import backend.team.ahachul_backend.api.lost.adapter.web.`in`.dto.*
import backend.team.ahachul_backend.api.lost.application.port.`in`.LostPostUseCase
import backend.team.ahachul_backend.common.annotation.Authentication
import backend.team.ahachul_backend.common.dto.PageInfoDto
import backend.team.ahachul_backend.common.response.CommonResponse
import org.springframework.data.domain.Pageable
import org.springframework.web.bind.annotation.*
import org.springframework.web.multipart.MultipartFile

Expand All @@ -21,10 +21,11 @@ class LostPostController(

@GetMapping("/v1/lost-posts")
fun searchLostPosts(
pageable: Pageable,
@RequestParam(required = false) pageToken: String?,
@RequestParam pageSize: Int,
request: SearchLostPostsDto.Request
): CommonResponse<SearchLostPostsDto.Response> {
return CommonResponse.success(lostPostService.searchLostPosts(request.toCommand(pageable)))
): CommonResponse<PageInfoDto<SearchLostPostsDto.Response>> {
return CommonResponse.success(lostPostService.searchLostPosts(request.toCommand(pageToken, pageSize)))
}

@Authentication
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,43 +4,40 @@ import backend.team.ahachul_backend.api.lost.application.service.command.`in`.Se
import backend.team.ahachul_backend.api.lost.domain.model.LostOrigin
import backend.team.ahachul_backend.api.lost.domain.model.LostStatus
import backend.team.ahachul_backend.api.lost.domain.model.LostType
import org.springframework.data.domain.Pageable

class SearchLostPostsDto {

data class Request(
val lostType: LostType,
val lostOrigin: LostOrigin,
val subwayLineId: Long?,
val origin: LostOrigin?,
val keyword: String?
val category: String?,
val keyword: String?,
) {
fun toCommand(pageable: Pageable): SearchLostPostCommand {
fun toCommand(pageToken: String?, pageSize: Int): SearchLostPostCommand {
return SearchLostPostCommand(
lostType = lostType,
lostOrigin = lostOrigin,
subwayLineId = subwayLineId,
lostOrigin = origin,
keyword = keyword,
pageable = pageable
category = category,
pageToken = pageToken,
pageSize = pageSize
)
}
}

data class Response(
val hasNext: Boolean,
val posts: List<SearchLost>
)

data class SearchLost(
val id: Long,
val title: String,
val content: String,
val writer: String?,
val createdBy: String,
val date: String,
val subwayLine: Long?,
val chats: Int = 0,
val createdAt: String,
val subwayLineId: Long?,
val chatCnt: Int = 0,
val status: LostStatus,
val imageUrl: String?,
val image: String?,
val categoryName: String?
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -10,9 +10,9 @@ import backend.team.ahachul_backend.api.lost.domain.model.LostPostType
import backend.team.ahachul_backend.api.lost.domain.model.LostStatus
import backend.team.ahachul_backend.api.lost.domain.model.LostType
import backend.team.ahachul_backend.common.domain.entity.SubwayLineEntity
import com.querydsl.core.types.ExpressionUtils.orderBy
import com.querydsl.core.types.dsl.Expressions
import com.querydsl.jpa.impl.JPAQueryFactory
import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Pageable
import org.springframework.data.domain.Slice
import org.springframework.data.domain.SliceImpl
Expand All @@ -39,59 +39,52 @@ class CustomLostPostRepository(
"values (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?);"

jdbcTemplate.batchUpdate(sql, lostPosts, lostPosts.size) { ps, lostPost ->
lostPost.member?.let { ps.setLong(1, it.id) } ?: ps.setNull(1, Types.BIGINT)
lostPost.subwayLine?.let { ps.setLong(2, it.id) } ?: ps.setNull(2, Types.BIGINT)
ps.setString(3, lostPost.title)
ps.setString(4, lostPost.content)
ps.setString(5, LostStatus.PROGRESS.name)
ps.setString(6, LostOrigin.LOST112.name)
ps.setString(7, LostPostType.CREATED.name)
ps.setString(8, LostType.ACQUIRE.name)
ps.setString(9, lostPost.storage)
ps.setString(10, lostPost.storageNumber)
ps.setTimestamp(11, currentTime)
ps.setString(12, "SYSTEM")
ps.setTimestamp(13, currentTime)
ps.setString(14, "SYSTEM")
ps.setString(15, lostPost.pageUrl)
lostPost.receivedDate?.let { ps.setTimestamp(16, Timestamp.valueOf(it)) } ?: ps.setNull(16, Types.VARCHAR)
lostPost.category?.let { ps.setLong(17, it.id) } ?: ps.setNull(17, Types.BIGINT)
lostPost.externalSourceFileUrl?.let { ps.setString(18, it) } ?: ps.setNull(18, Types.VARCHAR)
lostPost.member?.let { ps.setLong(1, it.id) } ?: ps.setNull(1, Types.BIGINT)
lostPost.subwayLine?.let { ps.setLong(2, it.id) } ?: ps.setNull(2, Types.BIGINT)
ps.setString(3, lostPost.title)
ps.setString(4, lostPost.content)
ps.setString(5, LostStatus.PROGRESS.name)
ps.setString(6, LostOrigin.LOST112.name)
ps.setString(7, LostPostType.CREATED.name)
ps.setString(8, LostType.ACQUIRE.name)
ps.setString(9, lostPost.storage)
ps.setString(10, lostPost.storageNumber)
ps.setTimestamp(11, currentTime)
ps.setString(12, "SYSTEM")
ps.setTimestamp(13, currentTime)
ps.setString(14, "SYSTEM")
ps.setString(15, lostPost.pageUrl)
lostPost.receivedDate.let { ps.setTimestamp(16, Timestamp.valueOf(it)) } ?: ps.setNull(16, Types.VARCHAR)
lostPost.category?.let { ps.setLong(17, it.id) } ?: ps.setNull(17, Types.BIGINT)
lostPost.externalSourceFileUrl?.let { ps.setString(18, it) } ?: ps.setNull(18, Types.VARCHAR)
}
}

fun searchLostPosts(command: GetSliceLostPostsCommand): Slice<LostPostEntity> {
val pageable = command.pageable
val response = queryFactory.selectFrom(lostPostEntity)
fun searchLostPosts(command: GetSliceLostPostsCommand): List<LostPostEntity> {
val orderSpecifier = if (command.lostOrigin == LostOrigin.LOST112) {
listOf(lostPostEntity.receivedDate.desc(), lostPostEntity.id.asc())
} else {
listOf(lostPostEntity.createdAt.desc(), lostPostEntity.id.asc())
}

val all = queryFactory.selectFrom(lostPostEntity).fetch()

return queryFactory.selectFrom(lostPostEntity)
.where(
lostOriginEq(command.lostOrigin),
subwayLineEq(command.subwayLine),
lostTypeEq(command.lostType),
lostOriginEq(command.lostOrigin),
categoryEq(command.category),
titleAndContentLike(command.keyword),
createdAtBeforeOrEqual(
command.lostOrigin,
command.date,
command.lostPostId
)
)
.orderBy(lostPostEntity.receivedDate.desc())
.offset(getOffset(pageable).toLong())
.limit((pageable.pageSize + 1).toLong())
.orderBy(*orderSpecifier.toTypedArray())
.limit((command.pageSize + 1).toLong())
.fetch()

return SliceImpl(response, pageable, hasNext(response, pageable.pageSize))
}

private fun getOffset(pageable: Pageable): Int {
return when {
pageable.pageNumber != 0 -> pageable.pageNumber * pageable.pageSize
else -> pageable.pageNumber
}
}

private fun hasNext(response: MutableList<LostPostEntity>, pageSize: Int): Boolean {
return when {
response.size > pageSize -> {
response.removeAt(response.size - 1)
true
}
else -> false
}
}

fun searchRecommendPost(command: GetRecommendLostPostsCommand): List<LostPostEntity> {
Expand All @@ -118,15 +111,15 @@ class CustomLostPostRepository(
.fetch()
}

private fun lostOriginEq(lostOrigin: LostOrigin?) =
lostOrigin?.let { lostPostEntity.origin.eq(lostOrigin) }

private fun subwayLineEq(subwayLine: SubwayLineEntity?) =
subwayLine?.let { lostPostEntity.subwayLine.eq(subwayLine) }

private fun lostTypeEq(lostType: LostType?) =
lostType?.let { lostPostEntity.lostType.eq(lostType) }

private fun lostOriginEq(lostOrigin: LostOrigin?) =
lostOrigin?.let { lostPostEntity.origin.eq(lostOrigin) }

private fun categoryEq(category: CategoryEntity?) =
category?.let { lostPostEntity.category.eq(category) }

Expand All @@ -138,4 +131,23 @@ class CustomLostPostRepository(
lostPostEntity.title.contains(keyword)
.or(lostPostEntity.content.contains(keyword))
}

private fun createdAtBeforeOrEqual(lostOrigin: LostOrigin, localDateTime: LocalDateTime?, id: Long?) =
if (lostOrigin == LostOrigin.LOST112) {
localDateTime?.let { date ->
id?.let { lostPostId ->
lostPostEntity.receivedDate.lt(date).or(
lostPostEntity.receivedDate.eq(date).and(lostPostEntity.id.gt(lostPostId))
)
}
}
} else {
localDateTime?.let { date ->
id?.let { lostPostId ->
lostPostEntity.createdAt.lt(date).or(
lostPostEntity.createdAt.eq(date).and(lostPostEntity.id.gt(lostPostId))
)
}
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ class LostPostPersistence(
.orElseThrow { throw AdapterException(ResponseCode.INVALID_DOMAIN) }
}

override fun getLostPosts(command: GetSliceLostPostsCommand): Slice<LostPostEntity> {
override fun getLostPosts(command: GetSliceLostPostsCommand): List<LostPostEntity> {
return customLostPostRepository.searchLostPosts(command)
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,13 @@ import backend.team.ahachul_backend.api.lost.adapter.web.`in`.dto.*
import backend.team.ahachul_backend.api.lost.application.service.command.`in`.CreateLostPostCommand
import backend.team.ahachul_backend.api.lost.application.service.command.`in`.SearchLostPostCommand
import backend.team.ahachul_backend.api.lost.application.service.command.`in`.UpdateLostPostCommand
import backend.team.ahachul_backend.common.dto.PageInfoDto

interface LostPostUseCase {

fun getLostPost(id: Long): GetLostPostDto.Response

fun searchLostPosts(command: SearchLostPostCommand): SearchLostPostsDto.Response
fun searchLostPosts(command: SearchLostPostCommand): PageInfoDto<SearchLostPostsDto.Response>

fun createLostPost(command: CreateLostPostCommand): CreateLostPostDto.Response

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ interface LostPostReader {

fun getLostPost(id: Long): LostPostEntity

fun getLostPosts(command: GetSliceLostPostsCommand): Slice<LostPostEntity>
fun getLostPosts(command: GetSliceLostPostsCommand): List<LostPostEntity>

fun getRecommendLostPosts(command: GetRecommendLostPostsCommand): List<LostPostEntity>

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import backend.team.ahachul_backend.api.lost.domain.model.LostOrigin
import backend.team.ahachul_backend.api.member.application.port.out.MemberReader
import backend.team.ahachul_backend.common.domain.entity.SubwayLineEntity
import backend.team.ahachul_backend.common.dto.ImageDto
import backend.team.ahachul_backend.common.dto.PageInfoDto
import backend.team.ahachul_backend.common.exception.BusinessException
import backend.team.ahachul_backend.common.persistence.SubwayLineReader
import backend.team.ahachul_backend.common.response.ResponseCode
Expand Down Expand Up @@ -53,7 +54,7 @@ class LostPostService(

private fun getRecommendPosts(subwayLine: SubwayLineEntity?, category:CategoryEntity?): List<LostPostEntity> {
if (subwayLine === null || category === null) {
throw BusinessException(ResponseCode.IMPOSSIBLE_RECOMMEND_LOST_POST)
return listOf()
}

val command = GetRecommendLostPostsCommand.from(DEFAULT_RECOMMEND_SIZE, subwayLine, category)
Expand Down Expand Up @@ -98,25 +99,36 @@ class LostPostService(
}
}

override fun searchLostPosts(command: SearchLostPostCommand): SearchLostPostsDto.Response {
override fun searchLostPosts(command: SearchLostPostCommand): PageInfoDto<SearchLostPostsDto.Response> {
val subwayLine = command.subwayLineId?.let { subwayLineReader.getById(it) }
val sliceObject = lostPostReader.getLostPosts(GetSliceLostPostsCommand.from(command, subwayLine))
val category = command.category?.let { categoryReader.getCategoryByName(it) }

val lostPosts = sliceObject.content.map {
SearchLostPostsDto.SearchLost(
val lostPostList = lostPostReader.getLostPosts(
GetSliceLostPostsCommand.from(
command=command, subwayLine=subwayLine, category=category
)
)

val searchLostPostsDtoList = lostPostList.map {
SearchLostPostsDto.Response(
id = it.id,
title = it.title,
content = it.content,
writer = it.member?.nickname,
createdBy = it.createdBy,
date = it.date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")),
subwayLine = it.subwayLine?.id,
createdAt = it.date.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS")),
subwayLineId = it.subwayLine?.id,
status = it.status,
imageUrl = getFileSource(it),
image = getFileSource(it),
categoryName = it.category?.name
)
}
return SearchLostPostsDto.Response(hasNext = sliceObject.hasNext(), posts = lostPosts)

return PageInfoDto.of(
data=searchLostPostsDtoList,
pageSize=command.pageSize,
arrayOf(SearchLostPostsDto.Response::createdAt, SearchLostPostsDto.Response::id)
)
}

private fun getFileSource(lostPost: LostPostEntity): String? {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ import backend.team.ahachul_backend.api.lost.domain.model.LostType
import org.springframework.web.multipart.MultipartFile

class CreateLostPostCommand(
// TODO 교환 희망 장소, 분실 세부 장소
val title: String,
val content: String,
val subwayLine: Long,
val lostType: LostType,
val categoryName: String,
var imageFiles: List<MultipartFile>? = listOf()
) {
}
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package backend.team.ahachul_backend.api.lost.application.service.command.`in`

import backend.team.ahachul_backend.api.lost.domain.model.LostOrigin
import backend.team.ahachul_backend.api.lost.domain.model.LostType
import org.springframework.data.domain.Pageable

class SearchLostPostCommand(
val lostType: LostType,
val lostOrigin: LostOrigin?,
val lostOrigin: LostOrigin,
val subwayLineId: Long?,
val category: String?,
val keyword: String?,
val pageable: Pageable
val pageToken: String?,
val pageSize: Int
)
Loading

0 comments on commit b18f622

Please sign in to comment.