From c77118fbfb9b753a83f4f4ddc3fc50d947789884 Mon Sep 17 00:00:00 2001 From: LeeJaeHyeok97 Date: Fri, 18 Apr 2025 03:47:32 +0900 Subject: [PATCH 1/2] =?UTF-8?q?[FEAT]=20elasticache=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=20=EB=B0=8F=20=EB=B0=A9=EB=AC=B8=EC=9E=90=EC=A0=95=EB=B3=B4(ip?= =?UTF-8?q?=20=EC=99=B8)=20=EC=88=98=EC=A7=91=20=EA=B8=B0=EB=8A=A5=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 5 ++- buildSrc/src/main/kotlin/Dependency.kt | 7 ++++ .../port/outbound/VisitorRepository.kt | 10 +++++ .../com/coffee/api/cafe/domain/Visitor.kt | 41 ++++++++++++++++++ .../cafe/infrastructure/VisitorConverter.kt | 30 +++++++++++++ .../persistence/entity/VisitorEntity.kt | 42 +++++++++++++++++++ .../repository/VisitorJpaRepository.kt | 12 ++++++ .../repository/VisitorRepositoryImpl.kt | 19 +++++++++ .../interceptor/SingleVisitInterceptor.kt | 30 +++++++++++++ .../adapter/in/scheduler/VisitorScheduler.kt | 40 ++++++++++++++++++ .../com/coffee/api/config/RedisConfig.kt | 31 ++++++++++++++ 11 files changed, 266 insertions(+), 1 deletion(-) create mode 100644 src/main/kotlin/com/coffee/api/cafe/application/port/outbound/VisitorRepository.kt create mode 100644 src/main/kotlin/com/coffee/api/cafe/domain/Visitor.kt create mode 100644 src/main/kotlin/com/coffee/api/cafe/infrastructure/VisitorConverter.kt create mode 100644 src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/entity/VisitorEntity.kt create mode 100644 src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorJpaRepository.kt create mode 100644 src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorRepositoryImpl.kt create mode 100644 src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/restapi/interceptor/SingleVisitInterceptor.kt create mode 100644 src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt create mode 100644 src/main/kotlin/com/coffee/api/config/RedisConfig.kt diff --git a/build.gradle.kts b/build.gradle.kts index 9c06531..3a693cb 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -78,7 +78,10 @@ dependencies { implementation(Dependency.JDSL.SPRING_DATA_JPA_SUPPORTER) // Discord - implementation (Dependency.Discord.WEB_HOOK) + implementation(Dependency.Discord.WEB_HOOK) + + // Redis + implementation(Dependency.Redis.STARTER_DATA) } kotlin { diff --git a/buildSrc/src/main/kotlin/Dependency.kt b/buildSrc/src/main/kotlin/Dependency.kt index a6da274..3753e7b 100644 --- a/buildSrc/src/main/kotlin/Dependency.kt +++ b/buildSrc/src/main/kotlin/Dependency.kt @@ -69,4 +69,11 @@ object Dependency { val WEB_HOOK = "$BASE:spring-boot-starter-webflux" } + + object Redis { + private const val REDIS_VERSION = "2.7.7" + private const val BASE = "org.springframework.boot" + + val STARTER_DATA = "$BASE:spring-boot-starter-data-redis:$REDIS_VERSION" + } } diff --git a/src/main/kotlin/com/coffee/api/cafe/application/port/outbound/VisitorRepository.kt b/src/main/kotlin/com/coffee/api/cafe/application/port/outbound/VisitorRepository.kt new file mode 100644 index 0000000..8d1375e --- /dev/null +++ b/src/main/kotlin/com/coffee/api/cafe/application/port/outbound/VisitorRepository.kt @@ -0,0 +1,10 @@ +package com.coffee.api.cafe.application.port.outbound + +import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity +import java.time.LocalDate + +interface VisitorRepository { + + fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean + fun save(visitor: VisitorEntity): VisitorEntity +} diff --git a/src/main/kotlin/com/coffee/api/cafe/domain/Visitor.kt b/src/main/kotlin/com/coffee/api/cafe/domain/Visitor.kt new file mode 100644 index 0000000..1469031 --- /dev/null +++ b/src/main/kotlin/com/coffee/api/cafe/domain/Visitor.kt @@ -0,0 +1,41 @@ +package com.coffee.api.cafe.domain + +import com.coffee.api.common.domain.AbstractDomain +import com.coffee.api.common.domain.UUIDTypeId +import com.fasterxml.jackson.annotation.JsonCreator +import java.time.LocalDate +import java.util.* + +class Visitor private constructor( + override val id: Id, + val userIp: String, + val userAgent: String, + val date: LocalDate +) : AbstractDomain() { + + companion object { + @JsonCreator + fun create( + id: UUID, + userIp: String, + userAgent: String, + date: LocalDate + ): Visitor { + return Visitor( + id = UUIDTypeId.from(id), + userIp = userIp, + userAgent = userAgent, + date = date + ) + } + + operator fun invoke( + id: UUID, + userIp: String, + userAgent: String, + date: LocalDate + ): Visitor = create(id, userIp, userAgent, date) + } + + data class Id(override val value: UUID) : UUIDTypeId(value) +} diff --git a/src/main/kotlin/com/coffee/api/cafe/infrastructure/VisitorConverter.kt b/src/main/kotlin/com/coffee/api/cafe/infrastructure/VisitorConverter.kt new file mode 100644 index 0000000..0c3c768 --- /dev/null +++ b/src/main/kotlin/com/coffee/api/cafe/infrastructure/VisitorConverter.kt @@ -0,0 +1,30 @@ +package com.coffee.api.cafe.infrastructure + +import com.coffee.api.cafe.domain.Visitor +import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity +import com.coffee.api.common.infrastructure.persistence.DomainEntityConverter +import org.springframework.stereotype.Component + +@Component +class VisitorConverter : DomainEntityConverter( + Visitor::class, + VisitorEntity::class +) { + override fun toDomain(entity: VisitorEntity): Visitor { + return Visitor( + id = entity.id, + userIp = entity.userIp, + userAgent = entity.userAgent, + date = entity.date + ) + } + + override fun toEntity(domain: Visitor): VisitorEntity { + return VisitorEntity( + id = domain.id.value, + userIp = domain.userIp, + userAgent = domain.userAgent, + date = domain.date + ) + } +} diff --git a/src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/entity/VisitorEntity.kt b/src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/entity/VisitorEntity.kt new file mode 100644 index 0000000..0b5edca --- /dev/null +++ b/src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/entity/VisitorEntity.kt @@ -0,0 +1,42 @@ +package com.coffee.api.cafe.infrastructure.persistence.entity + +import com.coffee.api.common.infrastructure.persistence.BaseEntity +import jakarta.persistence.Entity +import jakarta.persistence.Id +import jakarta.persistence.Table +import java.time.LocalDate +import java.util.UUID + +@Entity +@Table(name = "visitors") +class VisitorEntity( + id: UUID, + userIp: String, + userAgent: String, + date: LocalDate +) : BaseEntity() { + + @Id + var id: UUID = id + protected set + + var userIp: String = userIp + protected set + + var userAgent: String = userAgent + protected set + var date: LocalDate = date + protected set + + + companion object { + fun of(userIp: String, userAgent: String, date: LocalDate): VisitorEntity { + return VisitorEntity( + id = UUID.randomUUID(), + userIp = userIp, + userAgent = userAgent, + date = date + ) + } + } +} diff --git a/src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorJpaRepository.kt b/src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorJpaRepository.kt new file mode 100644 index 0000000..6d94b6d --- /dev/null +++ b/src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorJpaRepository.kt @@ -0,0 +1,12 @@ +package com.coffee.api.cafe.infrastructure.persistence.repository + +import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity +import org.springframework.data.jpa.repository.JpaRepository +import java.time.LocalDate +import java.util.UUID + +interface VisitorJpaRepository : JpaRepository { + fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean + + fun save(visitorEntity: VisitorEntity): VisitorEntity +} diff --git a/src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorRepositoryImpl.kt b/src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorRepositoryImpl.kt new file mode 100644 index 0000000..73ed99b --- /dev/null +++ b/src/main/kotlin/com/coffee/api/cafe/infrastructure/persistence/repository/VisitorRepositoryImpl.kt @@ -0,0 +1,19 @@ +package com.coffee.api.cafe.infrastructure.persistence.repository + +import com.coffee.api.cafe.application.port.outbound.VisitorRepository +import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity +import org.springframework.stereotype.Repository +import java.time.LocalDate + +@Repository +class VisitorRepositoryImpl( + private val visitorJpaRepository: VisitorJpaRepository +) : VisitorRepository { + override fun existsByUserIpAndDate(userIp: String, date: LocalDate): Boolean { + return visitorJpaRepository.existsByUserIpAndDate(userIp, date) + } + + override fun save(visitor: VisitorEntity): VisitorEntity { + return visitorJpaRepository.save(visitor) + } +} diff --git a/src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/restapi/interceptor/SingleVisitInterceptor.kt b/src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/restapi/interceptor/SingleVisitInterceptor.kt new file mode 100644 index 0000000..2de152e --- /dev/null +++ b/src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/restapi/interceptor/SingleVisitInterceptor.kt @@ -0,0 +1,30 @@ +package com.coffee.api.cafe.presentation.adapter.`in`.restapi.interceptor + +import jakarta.servlet.http.HttpServletRequest +import jakarta.servlet.http.HttpServletResponse +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.core.ValueOperations +import org.springframework.stereotype.Component +import org.springframework.web.servlet.HandlerInterceptor +import java.time.LocalDate + +@Component +class SingleVisitInterceptor( + private val redisTemplate: RedisTemplate +) : HandlerInterceptor { + + override fun preHandle(request: HttpServletRequest, response: HttpServletResponse, handler: Any): Boolean { + val userIp = request.remoteAddr + val userAgent = request.getHeader("User-Agent") + val today = LocalDate.now().toString() + val key = "${userIp}_${today}" + + val valueOperations: ValueOperations = redisTemplate.opsForValue() + + if (!redisTemplate.hasKey(key)) { + valueOperations.set(key, userAgent) + } + + return true + } +} diff --git a/src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt b/src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt new file mode 100644 index 0000000..9a77ada --- /dev/null +++ b/src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt @@ -0,0 +1,40 @@ +package com.coffee.api.cafe.presentation.adapter.`in`.scheduler + +import com.coffee.api.cafe.application.port.outbound.VisitorRepository +import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity +import org.slf4j.LoggerFactory +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.scheduling.annotation.Scheduled +import org.springframework.stereotype.Component +import java.time.LocalDate + +@Component +class VisitorScheduler( + private val redisTemplate: RedisTemplate, + private val visitorRepository: VisitorRepository +) { + + private val log = LoggerFactory.getLogger(this::class.java) + + @Scheduled(initialDelay = 3000000, fixedDelay = 3000000) + fun updateVisitorData() { + val keys = redisTemplate.keys("*_*") ?: return + + for (key in keys) { + val parts = key.split("_") + if (parts.size != 2) continue + + val userIp = parts[0] + val date = runCatching { LocalDate.parse(parts[1]) }.getOrNull() ?: continue + + val userAgent = redisTemplate.opsForValue().get(key) ?: continue + + if (!visitorRepository.existsByUserIpAndDate(userIp, date)) { + val visitor = VisitorEntity.of(userIp, userAgent, date) + visitorRepository.save(visitor) + } + + redisTemplate.delete(key) + } + } +} diff --git a/src/main/kotlin/com/coffee/api/config/RedisConfig.kt b/src/main/kotlin/com/coffee/api/config/RedisConfig.kt new file mode 100644 index 0000000..3cf5979 --- /dev/null +++ b/src/main/kotlin/com/coffee/api/config/RedisConfig.kt @@ -0,0 +1,31 @@ +package com.coffee.api.config + +import org.springframework.beans.factory.annotation.Value +import org.springframework.context.annotation.Bean +import org.springframework.context.annotation.Configuration +import org.springframework.data.redis.connection.RedisConnectionFactory +import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory +import org.springframework.data.redis.core.RedisTemplate +import org.springframework.data.redis.repository.configuration.EnableRedisRepositories +import java.net.URI + +@Configuration +@EnableRedisRepositories +class RedisConfig( + @Value("\${spring.data.redis.url}") + private val redisUrl: String +) { + + @Bean + fun redisConnectionFactory(): RedisConnectionFactory { + val uri = URI(redisUrl) + return LettuceConnectionFactory(uri.host, uri.port) + } + + @Bean + fun redisTemplate(): RedisTemplate { + val template = RedisTemplate() + template.connectionFactory = redisConnectionFactory() + return template + } +} From 8b7d758b7efb03b5b9744a8c7615abe34b6d2a84 Mon Sep 17 00:00:00 2001 From: LeeJaeHyeok97 Date: Sat, 19 Apr 2025 17:24:53 +0900 Subject: [PATCH 2/2] =?UTF-8?q?[CHORE]=20=EC=9B=8C=ED=81=AC=ED=94=8C?= =?UTF-8?q?=EB=A1=9C=EC=9A=B0=20=EC=88=98=EC=A0=95=20=EB=B0=8F=20=EA=B0=9C?= =?UTF-8?q?=EB=B0=9C=20=ED=99=98=EA=B2=BD=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/deploy.yml | 6 ++++++ .gitignore | 4 +++- buildSrc/gradlew | 0 gradlew | 0 scripts/local-develop-env-reset | 0 .../presentation/adapter/in/scheduler/VisitorScheduler.kt | 1 + src/main/kotlin/com/coffee/api/config/RedisConfig.kt | 1 + 7 files changed, 11 insertions(+), 1 deletion(-) mode change 100755 => 100644 buildSrc/gradlew mode change 100755 => 100644 gradlew mode change 100755 => 100644 scripts/local-develop-env-reset diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 20da11d..0a76524 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -28,6 +28,12 @@ jobs: touch ./src/main/resources/application.yml echo "${{ secrets.APPLICATION_YML }}" >> src/main/resources/application.yml + - name: create application-prod.yml file + run: echo "${{ secrets.APPLICATION_PROD_YML }}" > src/main/resources/application-prod.yml + + - name: create application-local.yml file + run: echo "${{ secrets.APPLICATION_LOCAL_YML }}" > src/main/resources/application-local.yml + - name: Grant execute permission for gradlew run: chmod +x ./gradlew shell: bash diff --git a/.gitignore b/.gitignore index c3743f6..17a33e2 100644 --- a/.gitignore +++ b/.gitignore @@ -41,4 +41,6 @@ out/ .env -src/main/resources/application.yml \ No newline at end of file +src/main/resources/application.yml +src/main/resources/application-local.yml +src/main/resources/application-prod.yml \ No newline at end of file diff --git a/buildSrc/gradlew b/buildSrc/gradlew old mode 100755 new mode 100644 diff --git a/gradlew b/gradlew old mode 100755 new mode 100644 diff --git a/scripts/local-develop-env-reset b/scripts/local-develop-env-reset old mode 100755 new mode 100644 diff --git a/src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt b/src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt index 9a77ada..6723c50 100644 --- a/src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt +++ b/src/main/kotlin/com/coffee/api/cafe/presentation/adapter/in/scheduler/VisitorScheduler.kt @@ -3,6 +3,7 @@ package com.coffee.api.cafe.presentation.adapter.`in`.scheduler import com.coffee.api.cafe.application.port.outbound.VisitorRepository import com.coffee.api.cafe.infrastructure.persistence.entity.VisitorEntity import org.slf4j.LoggerFactory +import org.springframework.context.annotation.Profile import org.springframework.data.redis.core.RedisTemplate import org.springframework.scheduling.annotation.Scheduled import org.springframework.stereotype.Component diff --git a/src/main/kotlin/com/coffee/api/config/RedisConfig.kt b/src/main/kotlin/com/coffee/api/config/RedisConfig.kt index 3cf5979..a5870c2 100644 --- a/src/main/kotlin/com/coffee/api/config/RedisConfig.kt +++ b/src/main/kotlin/com/coffee/api/config/RedisConfig.kt @@ -3,6 +3,7 @@ package com.coffee.api.config import org.springframework.beans.factory.annotation.Value import org.springframework.context.annotation.Bean import org.springframework.context.annotation.Configuration +import org.springframework.context.annotation.Profile import org.springframework.data.redis.connection.RedisConnectionFactory import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory import org.springframework.data.redis.core.RedisTemplate