Skip to content

Commit

Permalink
feature: 배너 이미지 조회 (#18)
Browse files Browse the repository at this point in the history
* feat: Banner 도메인 추가

* chore: Spring Cache 설정 추가

* feat: Banner 조회 로직 추가

* feat: Banner 조회 API 추가

* test: Banner Api test 추가

* test: Api test 공통화

* chore: 배포 오류 수정
- ddl-auto option으로 table 생성
- docker compose시 이전 Image 삭제 옵션 추가

* chore: CI scope 수정
- only push

* refactor: Api resource url 수정

* style: method, dto naming 수정

* style: 변수명에 자료형 제거

* refactor: 불필요한 응답 필드 제거

* style: static import 제거

* test: SoftAssertions 적용
  • Loading branch information
TaeyeonRoyce authored and hgo641 committed Jan 28, 2024
1 parent 43a251e commit 009223e
Show file tree
Hide file tree
Showing 13 changed files with 231 additions and 8 deletions.
9 changes: 4 additions & 5 deletions .github/workflows/deploy-prod.yml
Original file line number Diff line number Diff line change
Expand Up @@ -45,9 +45,9 @@ jobs:
tags: ${{ secrets.DOCKER_HUB_REPOSITORY }}:prod

deployment:
runs-on: ubuntu-latest
needs: [ build-docker-image-and-push ]
steps:
runs-on: ubuntu-latest
needs: [ build-docker-image-and-push ]
steps:
- name: Deploy
uses: appleboy/ssh-action@master
with:
Expand All @@ -57,6 +57,5 @@ jobs:
script: |
echo ${{ secrets.DOCKERHUB_TOKEN }} | docker login -u ${{ secrets.DOCKERHUB_USERNAME }} --password-stdin
cd api/bin/
sudo docker compose down
sudo docker compose down --rmi all
sudo docker compose up -d
sudo docker image prune -af
2 changes: 0 additions & 2 deletions .github/workflows/gradle-CI.yml
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ name: CI With Pull Request

on:
push:
pull_request:
types: [opened, reopened]

jobs:
build:
Expand Down
2 changes: 1 addition & 1 deletion backend-submodule
3 changes: 3 additions & 0 deletions build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -30,9 +30,12 @@ dependencies {
implementation("com.linecorp.kotlin-jdsl:jpql-dsl:3.3.0")
implementation("com.linecorp.kotlin-jdsl:jpql-render:3.3.0")

implementation("org.springframework.boot:spring-boot-starter-cache")

implementation("io.jsonwebtoken:jjwt-api:0.11.5")
runtimeOnly("com.mysql:mysql-connector-j")
runtimeOnly("com.h2database:h2")

annotationProcessor("org.springframework.boot:spring-boot-configuration-processor")
testImplementation("org.springframework.boot:spring-boot-starter-test")
testImplementation("io.kotest:kotest-runner-junit5:5.4.2")
Expand Down
21 changes: 21 additions & 0 deletions src/main/kotlin/com/petqua/application/banner/BannerService.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.petqua.application.banner

import com.petqua.application.banner.dto.BannerResponse
import com.petqua.domain.banner.BannerRepository
import org.springframework.cache.annotation.Cacheable
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

@Transactional
@Service
class BannerService(
private val bannerRepository: BannerRepository,
) {

@Cacheable("banners")
@Transactional(readOnly = true)
fun readAll(): List<BannerResponse> {
val banners = bannerRepository.findAll()
return banners.map { BannerResponse.from(it) }
}
}
19 changes: 19 additions & 0 deletions src/main/kotlin/com/petqua/application/banner/dto/BannerDtos.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.petqua.application.banner.dto

import com.petqua.domain.banner.Banner

data class BannerResponse(
val id: Long,
val imageUrl: String,
val linkUrl: String,
) {
companion object {
fun from(banner: Banner): BannerResponse {
return BannerResponse(
id = banner.id,
imageUrl = banner.imageUrl,
linkUrl = banner.linkUrl,
)
}
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/com/petqua/common/cofig/CacheConfiguration.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.petqua.common.cofig

import org.springframework.cache.CacheManager
import org.springframework.cache.annotation.EnableCaching
import org.springframework.cache.concurrent.ConcurrentMapCacheManager
import org.springframework.context.annotation.Bean
import org.springframework.context.annotation.Configuration


@EnableCaching
@Configuration
class CacheConfiguration {

@Bean
fun cacheManager(): CacheManager {
val cacheManager = ConcurrentMapCacheManager()
cacheManager.setCacheNames(listOf("banners"))
return cacheManager
}
}
20 changes: 20 additions & 0 deletions src/main/kotlin/com/petqua/domain/banner/Banner.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.petqua.domain.banner

import com.petqua.common.domain.BaseEntity
import jakarta.persistence.Column
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id

@Entity
class Banner(
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long = 0L,

@Column(nullable = false)
val imageUrl: String,

@Column(nullable = false)
val linkUrl: String,
) : BaseEntity()
6 changes: 6 additions & 0 deletions src/main/kotlin/com/petqua/domain/banner/BannerRepository.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.petqua.domain.banner

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

interface BannerRepository: JpaRepository<Banner, Long> {
}
21 changes: 21 additions & 0 deletions src/main/kotlin/com/petqua/presentation/banner/BannerController.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.petqua.presentation.banner

import com.petqua.application.banner.BannerService
import com.petqua.application.banner.dto.BannerResponse
import org.springframework.http.ResponseEntity
import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.RestController

@RequestMapping("/banners")
@RestController
class BannerController(
private val bannerService: BannerService
) {

@GetMapping
fun readAll(): ResponseEntity<List<BannerResponse>> {
val response = bannerService.readAll()
return ResponseEntity.ok(response)
}
}
44 changes: 44 additions & 0 deletions src/test/kotlin/com/petqua/application/banner/BannerServiceTest.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
package com.petqua.application.banner

import com.petqua.domain.banner.Banner
import com.petqua.domain.banner.BannerRepository
import io.kotest.core.spec.style.BehaviorSpec
import io.kotest.matchers.shouldBe
import org.mockito.Mockito.atMost
import org.mockito.Mockito.verify
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.NONE
import org.springframework.boot.test.mock.mockito.SpyBean

@SpringBootTest(webEnvironment = NONE)
class BannerServiceTest(
private var bannerService: BannerService,
@SpyBean private var bannerRepository: BannerRepository,
) : BehaviorSpec({

Given("Banner 조회 테스트") {
bannerRepository.saveAll(
listOf(
Banner(imageUrl = "imageUrlA", linkUrl = "linkUrlA"),
Banner(imageUrl = "imageUrlB", linkUrl = "linkUrlB"),
Banner(imageUrl = "imageUrlC", linkUrl = "linkUrlC"),
)
)

When("Banner를 전체 조회 하면") {
val results = bannerService.readAll()

Then("모든 Banner가 조회 된다") {
results.size shouldBe 3
}
}

When("Banner가 캐싱 되어 있으면") {
repeat(5) { bannerService.readAll() }

Then("퀴리가 발생 하지 않는다") {
verify(bannerRepository, atMost(1)).findAll()
}
}
}
})
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package com.petqua.presentation.banner

import com.petqua.application.banner.dto.BannerResponse
import com.petqua.domain.banner.Banner
import com.petqua.domain.banner.BannerRepository
import com.petqua.test.ApiTestConfig
import io.restassured.module.kotlin.extensions.Extract
import io.restassured.module.kotlin.extensions.Given
import io.restassured.module.kotlin.extensions.Then
import io.restassured.module.kotlin.extensions.When
import org.assertj.core.api.SoftAssertions.assertSoftly
import org.springframework.http.HttpStatus

class BannerControllerTest(
private val bannerRepository: BannerRepository
) : ApiTestConfig() {
init {
Given("배너가 등록되어 있다.") {
val banner = bannerRepository.saveAll(
listOf(
Banner(imageUrl = "imageUrlC", linkUrl = "linkUrlA"),
Banner(imageUrl = "imageUrlB", linkUrl = "linkUrlB")
)
)

When("배너 목록을 조회한다.") {
val response = Given {
log().all()
} When {
get("/banners")
} Then {
log().all()
} Extract {
response()
}

Then("배너 목록을 응답한다.") {
val findBannerResponse = response.`as`(Array<BannerResponse>::class.java)

assertSoftly {
it.assertThat(response.statusCode).isEqualTo(HttpStatus.OK.value())
it.assertThat(findBannerResponse.size).isEqualTo(2)
}
}
}
}
}
}
24 changes: 24 additions & 0 deletions src/test/kotlin/com/petqua/test/ApiTestConfig.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.petqua.test

import io.kotest.core.spec.style.BehaviorSpec
import io.restassured.RestAssured
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT
import org.springframework.boot.test.web.server.LocalServerPort

@SpringBootTest(webEnvironment = RANDOM_PORT)
abstract class ApiTestConfig : BehaviorSpec() {

@LocalServerPort
protected val port: Int = RestAssured.port

@Autowired
private lateinit var dataCleaner: DataCleaner

init {
afterContainer {
dataCleaner.clean()
}
}
}

0 comments on commit 009223e

Please sign in to comment.