Skip to content

Commit

Permalink
feat: 셀럽 단일 조회 api (#43)
Browse files Browse the repository at this point in the history
* feat: 셀럽 단일 조회 유즈케이스 정의

* feat: 셀럽 단일 조회 쿼리 추가

* feat: 셀럽 단일 조회 로직 구현

* feat: 셀럽 단일 조회 API 추가

* style: ktlint
  • Loading branch information
TaeyeonRoyce authored Aug 15, 2024
1 parent b46e502 commit 5c25546
Show file tree
Hide file tree
Showing 13 changed files with 328 additions and 1 deletion.
14 changes: 14 additions & 0 deletions src/main/kotlin/com/celuveat/celeb/adapter/in/rest/CelebrityApi.kt
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ import com.celuveat.auth.adapter.`in`.rest.Auth
import com.celuveat.auth.adapter.`in`.rest.AuthContext
import com.celuveat.celeb.adapter.`in`.rest.response.BestCelebrityResponse
import com.celuveat.celeb.adapter.`in`.rest.response.CelebrityResponse
import com.celuveat.celeb.adapter.`in`.rest.response.CelebrityWithInterestedResponse
import io.swagger.v3.oas.annotations.Operation
import io.swagger.v3.oas.annotations.Parameter
import io.swagger.v3.oas.annotations.enums.ParameterIn
Expand Down Expand Up @@ -56,4 +57,17 @@ interface CelebrityApi {
fun readBestCelebrities(
@Auth auth: AuthContext,
): List<BestCelebrityResponse>

@Operation(summary = "셀럽 정보 조회")
@GetMapping("/{celebrityId}")
fun readCelebrity(
@Auth auth: AuthContext,
@Parameter(
`in` = ParameterIn.PATH,
description = "셀럽 ID",
example = "1",
required = true,
)
@PathVariable celebrityId: Long,
): CelebrityWithInterestedResponse
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,12 +4,15 @@ import com.celuveat.auth.adapter.`in`.rest.Auth
import com.celuveat.auth.adapter.`in`.rest.AuthContext
import com.celuveat.celeb.adapter.`in`.rest.response.BestCelebrityResponse
import com.celuveat.celeb.adapter.`in`.rest.response.CelebrityResponse
import com.celuveat.celeb.adapter.`in`.rest.response.CelebrityWithInterestedResponse
import com.celuveat.celeb.application.port.`in`.AddInterestedCelebrityUseCase
import com.celuveat.celeb.application.port.`in`.DeleteInterestedCelebrityUseCase
import com.celuveat.celeb.application.port.`in`.ReadBestCelebritiesUseCase
import com.celuveat.celeb.application.port.`in`.ReadCelebrityUseCase
import com.celuveat.celeb.application.port.`in`.ReadInterestedCelebritiesUseCase
import com.celuveat.celeb.application.port.`in`.command.AddInterestedCelebrityCommand
import com.celuveat.celeb.application.port.`in`.command.DeleteInterestedCelebrityCommand
import com.celuveat.celeb.application.port.`in`.query.ReadCelebrityQuery
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.PathVariable
Expand All @@ -24,6 +27,7 @@ class CelebrityController(
private val readBestCelebritiesUseCase: ReadBestCelebritiesUseCase,
private val addInterestedCelebrityUseCase: AddInterestedCelebrityUseCase,
private val deleteInterestedCelebrityUseCase: DeleteInterestedCelebrityUseCase,
private val readCelebrityUseCase: ReadCelebrityUseCase,
) : CelebrityApi {
@GetMapping("/interested")
override fun readInterestedCelebrities(
Expand Down Expand Up @@ -62,4 +66,15 @@ class CelebrityController(
val celebritiesResults = readBestCelebritiesUseCase.readBestCelebrities(optionalMemberId)
return celebritiesResults.map { BestCelebrityResponse.from(it) }
}

@GetMapping("/{celebrityId}")
override fun readCelebrity(
@Auth auth: AuthContext,
@PathVariable celebrityId: Long,
): CelebrityWithInterestedResponse {
val memberId = auth.optionalMemberId()
val query = ReadCelebrityQuery(memberId, celebrityId)
val celebrityResult = readCelebrityUseCase.readCelebrity(query)
return CelebrityWithInterestedResponse.from(celebrityResult)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package com.celuveat.celeb.adapter.`in`.rest.response

import com.celuveat.celeb.application.port.`in`.result.BestCelebrityResult
import com.celuveat.celeb.application.port.`in`.result.CelebrityResult
import com.celuveat.celeb.application.port.`in`.result.CelebrityWithInterestedResult
import com.celuveat.celeb.application.port.`in`.result.SimpleCelebrityResult
import com.celuveat.celeb.application.port.`in`.result.YoutubeContentResult
import com.celuveat.restaurant.adapter.`in`.rest.response.RestaurantPreviewResponse
Expand Down Expand Up @@ -153,3 +154,24 @@ data class BestCelebrityResponse(
}
}
}

data class CelebrityWithInterestedResponse(
@Schema(
description = "연예인 정보",
)
val celebrity: CelebrityResponse,
@Schema(
description = "관심 여부",
example = "true",
)
val interested: Boolean,
) {
companion object {
fun from(result: CelebrityWithInterestedResult): CelebrityWithInterestedResponse {
return CelebrityWithInterestedResponse(
celebrity = CelebrityResponse.from(result.celebrity),
interested = result.interested,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,11 @@ class CelebrityPersistenceAdapter(
celebrityPersistenceMapper.toDomainWithoutYoutubeContent(it)
}
}

override fun readById(celebrityId: Long): Celebrity {
val celebrity = celebrityJpaRepository.getById(celebrityId)
val youtubeContents = celebrityYoutubeContentJpaRepository.findByCelebrity(celebrity)
.map { it.youtubeContent }
return celebrityPersistenceMapper.toDomain(celebrity, youtubeContents)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,7 @@ import org.springframework.data.jpa.repository.JpaRepository
interface CelebrityYoutubeContentJpaRepository : JpaRepository<CelebrityYoutubeContentJpaEntity, Long> {
@EntityGraph(attributePaths = ["youtubeContent"])
fun findByCelebrityIdIn(celebrityId: List<Long>): List<CelebrityYoutubeContentJpaEntity>

@EntityGraph(attributePaths = ["youtubeContent"])
fun findByCelebrity(celebrity: CelebrityJpaEntity): List<CelebrityYoutubeContentJpaEntity>
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
package com.celuveat.celeb.application

import com.celuveat.celeb.application.port.`in`.ReadBestCelebritiesUseCase
import com.celuveat.celeb.application.port.`in`.ReadCelebrityUseCase
import com.celuveat.celeb.application.port.`in`.ReadInterestedCelebritiesUseCase
import com.celuveat.celeb.application.port.`in`.query.ReadCelebrityQuery
import com.celuveat.celeb.application.port.`in`.result.BestCelebrityResult
import com.celuveat.celeb.application.port.`in`.result.CelebrityResult
import com.celuveat.celeb.application.port.`in`.result.CelebrityWithInterestedResult
import com.celuveat.celeb.application.port.`in`.result.SimpleCelebrityResult
import com.celuveat.celeb.application.port.out.ReadCelebritiesPort
import com.celuveat.celeb.application.port.out.ReadInterestedCelebritiesPort
Expand All @@ -19,7 +22,7 @@ class CelebrityQueryService(
private val readRestaurantPort: ReadRestaurantPort,
private val readInterestedCelebritiesPort: ReadInterestedCelebritiesPort,
private val readInterestedRestaurantPort: ReadInterestedRestaurantPort,
) : ReadInterestedCelebritiesUseCase, ReadBestCelebritiesUseCase {
) : ReadInterestedCelebritiesUseCase, ReadBestCelebritiesUseCase, ReadCelebrityUseCase {
override fun getInterestedCelebrities(memberId: Long): List<CelebrityResult> {
val celebrities = readInterestedCelebritiesPort.readInterestedCelebrities(memberId)
return celebrities.map { CelebrityResult.from(it.celebrity) }
Expand Down Expand Up @@ -60,4 +63,15 @@ class CelebrityQueryService(
).map { interested -> interested.restaurant }.toSet()
} ?: emptySet()
}

override fun readCelebrity(query: ReadCelebrityQuery): CelebrityWithInterestedResult {
val celebrity = readCelebritiesPort.readById(query.celebrityId)
val interested = query.memberId?.let {
readInterestedCelebritiesPort.existsInterestedCelebrity(it, query.celebrityId)
} ?: false
return CelebrityWithInterestedResult.of(
celebrity = celebrity,
isInterested = interested,
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.celuveat.celeb.application.port.`in`

import com.celuveat.celeb.application.port.`in`.query.ReadCelebrityQuery
import com.celuveat.celeb.application.port.`in`.result.CelebrityWithInterestedResult

interface ReadCelebrityUseCase {
fun readCelebrity(query: ReadCelebrityQuery): CelebrityWithInterestedResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.celuveat.celeb.application.port.`in`.query

data class ReadCelebrityQuery(
val memberId: Long?,
val celebrityId: Long,
)
Original file line number Diff line number Diff line change
Expand Up @@ -43,3 +43,20 @@ data class BestCelebrityResult(
val celebrity: SimpleCelebrityResult,
val restaurants: List<RestaurantPreviewResult>,
)

data class CelebrityWithInterestedResult(
val celebrity: CelebrityResult,
val interested: Boolean,
) {
companion object {
fun of(
celebrity: Celebrity,
isInterested: Boolean,
): CelebrityWithInterestedResult {
return CelebrityWithInterestedResult(
celebrity = CelebrityResult.from(celebrity),
interested = isInterested,
)
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,4 +6,6 @@ interface ReadCelebritiesPort {
fun readVisitedCelebritiesByRestaurants(restaurantIds: List<Long>): Map<Long, List<Celebrity>>

fun readBestCelebrities(): List<Celebrity>

fun readById(celebrityId: Long): Celebrity
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,18 @@ package com.celuveat.celeb.adapter.`in`.rest

import com.celuveat.auth.application.port.`in`.ExtractMemberIdUseCase
import com.celuveat.celeb.adapter.`in`.rest.response.BestCelebrityResponse
import com.celuveat.celeb.adapter.`in`.rest.response.CelebrityWithInterestedResponse
import com.celuveat.celeb.application.port.`in`.AddInterestedCelebrityUseCase
import com.celuveat.celeb.application.port.`in`.DeleteInterestedCelebrityUseCase
import com.celuveat.celeb.application.port.`in`.ReadBestCelebritiesUseCase
import com.celuveat.celeb.application.port.`in`.ReadCelebrityUseCase
import com.celuveat.celeb.application.port.`in`.ReadInterestedCelebritiesUseCase
import com.celuveat.celeb.application.port.`in`.command.AddInterestedCelebrityCommand
import com.celuveat.celeb.application.port.`in`.command.DeleteInterestedCelebrityCommand
import com.celuveat.celeb.application.port.`in`.query.ReadCelebrityQuery
import com.celuveat.celeb.application.port.`in`.result.BestCelebrityResult
import com.celuveat.celeb.application.port.`in`.result.CelebrityResult
import com.celuveat.celeb.application.port.`in`.result.CelebrityWithInterestedResult
import com.celuveat.support.sut
import com.fasterxml.jackson.databind.ObjectMapper
import com.navercorp.fixturemonkey.kotlin.giveMeBuilder
Expand All @@ -31,6 +35,7 @@ class CelebrityControllerTest(
@MockkBean val addInterestedCelebrityUseCase: AddInterestedCelebrityUseCase,
@MockkBean val deleteInterestedCelebrityUseCase: DeleteInterestedCelebrityUseCase,
@MockkBean val readBestCelebritiesUseCase: ReadBestCelebritiesUseCase,
@MockkBean val readCelebrityUseCase: ReadCelebrityUseCase,
// for AuthMemberArgumentResolver
@MockkBean val extractMemberIdUseCase: ExtractMemberIdUseCase,
) : FunSpec({
Expand Down Expand Up @@ -110,4 +115,40 @@ class CelebrityControllerTest(
}
}
}

context("단일 셀럽을 조회 한다") {
val celebrityId = 1L
val celebrityResult = sut.giveMeBuilder<CelebrityResult>().sample()

test("회원 조회 성공") {
val memberId = 1L
val accessToken = "celuveatAccessToken"
val query = ReadCelebrityQuery(memberId, celebrityId)
val result = CelebrityWithInterestedResult(celebrityResult, true)
every { extractMemberIdUseCase.extract(accessToken) } returns memberId
every { readCelebrityUseCase.readCelebrity(query) } returns result

mockMvc.get("/celebrities/{celebrityId}", celebrityId) {
header("Authorization", "Bearer $accessToken")
}.andExpect {
status { isOk() }
content { json(mapper.writeValueAsString(CelebrityWithInterestedResponse.from(result))) }
}.andDo {
print()
}
}

test("비회원 조회 성공") {
val query = ReadCelebrityQuery(null, celebrityId)
val result = CelebrityWithInterestedResult(celebrityResult, false)
every { readCelebrityUseCase.readCelebrity(query) } returns result

mockMvc.get("/celebrities/{celebrityId}", celebrityId).andExpect {
status { isOk() }
content { json(mapper.writeValueAsString(CelebrityWithInterestedResponse.from(result))) }
}.andDo {
print()
}
}
}
})
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import com.celuveat.restaurant.adapter.out.persistence.entity.RestaurantJpaRepos
import com.celuveat.support.PersistenceAdapterTest
import com.celuveat.support.sut
import com.navercorp.fixturemonkey.kotlin.giveMeBuilder
import com.navercorp.fixturemonkey.kotlin.giveMeOne
import com.navercorp.fixturemonkey.kotlin.set
import io.kotest.assertions.assertSoftly
import io.kotest.core.spec.style.FunSpec
Expand Down Expand Up @@ -134,6 +135,30 @@ class CelebrityPersistenceAdapterTest(
celebrities.size shouldBe 3
celebrities.map { it.id } shouldContainExactly listOf(celebrityC.id, celebrityA.id, celebrityB.id)
}

test("셀럽을 조회 한다.") {
// given
val celebrity = celebrityJpaRepository.save(sut.giveMeOne<CelebrityJpaEntity>())
val contentA = sut.giveMeBuilder<YoutubeContentJpaEntity>()
.set(YoutubeContentJpaEntity::channelId, "@channelAId")
.sample()
val contentB = sut.giveMeBuilder<YoutubeContentJpaEntity>()
.set(YoutubeContentJpaEntity::channelId, "@channelBId")
.sample()
val savedContents = youtubeContentJpaRepository.saveAll(listOf(contentA, contentB))
celebrityYoutubeContentJpaRepository.saveAll(
listOf(
generateCelebrityYoutubeContent(celebrity, savedContents[0]),
generateCelebrityYoutubeContent(celebrity, savedContents[1]),
),
)

// when
val findCelebrity = celebrityPersistenceAdapter.readById(celebrity.id)

// then
celebrity.id shouldBe findCelebrity.id
}
})

private fun generateCelebrityYoutubeContent(
Expand Down
Loading

0 comments on commit 5c25546

Please sign in to comment.