Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ import jakarta.persistence.OneToOne
import jakarta.persistence.Table
import jakarta.persistence.UniqueConstraint
import org.springframework.data.jpa.repository.JpaRepository
import team.cklob.arena.domain.market.MarketType
import team.cklob.arena.domain.market.domain.type.MarketType
import team.cklob.arena.domain.user.domain.entity.User
import java.math.BigDecimal
import java.time.LocalDateTime
Expand Down
72 changes: 0 additions & 72 deletions src/main/kotlin/team/cklob/arena/domain/market/Market.kt

This file was deleted.

16 changes: 16 additions & 0 deletions src/main/kotlin/team/cklob/arena/domain/market/MarketErrorCode.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package team.cklob.arena.domain.market

import org.springframework.http.HttpStatus
import team.cklob.arena.global.exception.ErrorCode

enum class MarketErrorCode(
override val status: HttpStatus,
override val message: String,
) : ErrorCode {
MARKET_NOT_ACTIVE(HttpStatus.CONFLICT, "현재 이용할 수 없는 시장입니다."),
SYMBOL_NOT_FOUND(HttpStatus.NOT_FOUND, "종목을 찾을 수 없습니다."),
SYMBOL_NOT_ACTIVE(HttpStatus.CONFLICT, "현재 이용할 수 없는 종목입니다."),
MARKET_DATA_UNAVAILABLE(HttpStatus.SERVICE_UNAVAILABLE, "시세 정보를 일시적으로 조회할 수 없습니다."),
SYMBOL_SYNC_REJECTED(HttpStatus.SERVICE_UNAVAILABLE, "종목 동기화 응답이 불완전하여 기존 데이터를 유지합니다."),
INVALID_PRICE_HISTORY_RANGE(HttpStatus.BAD_REQUEST, "가격 조회 기간은 최대 7일이며 시작일이 종료일보다 늦을 수 없습니다."),
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package team.cklob.arena.domain.market.application

import team.cklob.arena.domain.market.application.result.CurrentPriceResult

interface GetCurrentPriceService {
fun execute(symbolId: Long): CurrentPriceResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package team.cklob.arena.domain.market.application

import team.cklob.arena.domain.market.application.result.PriceHistoryResult
import java.time.LocalDate

interface GetPriceHistoryService {
fun execute(
symbolId: Long,
from: LocalDate,
to: LocalDate,
): PriceHistoryResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package team.cklob.arena.domain.market.application

import team.cklob.arena.domain.market.application.result.SymbolPageResult
import team.cklob.arena.domain.market.domain.type.MarketType

interface GetSymbolsService {
fun execute(
market: MarketType,
page: Int,
size: Int,
): SymbolPageResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package team.cklob.arena.domain.market.application

import team.cklob.arena.domain.market.application.result.CurrentPriceResult
import team.cklob.arena.domain.market.application.result.PriceHistoryResult
import team.cklob.arena.domain.market.domain.type.MarketType

interface MarketDataCache {
fun findCurrentPrice(symbolId: Long): CurrentPriceResult?

fun saveCurrentPrice(
market: MarketType,
result: CurrentPriceResult,
)

fun findPriceHistory(
symbolId: Long,
from: String,
to: String,
): PriceHistoryResult?

fun savePriceHistory(result: PriceHistoryResult)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package team.cklob.arena.domain.market.application

import team.cklob.arena.domain.market.application.result.ExternalSymbolResult
import team.cklob.arena.domain.market.application.result.PricePointResult
import team.cklob.arena.domain.market.application.result.PriceQuoteResult
import java.time.LocalDate

interface MarketDataClient {
fun fetchSymbols(): List<ExternalSymbolResult>

fun fetchCurrentPrice(code: String): PriceQuoteResult

fun fetchPriceHistory(
code: String,
from: LocalDate,
to: LocalDate,
): List<PricePointResult>
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package team.cklob.arena.domain.market.application

import org.springframework.stereotype.Component
import team.cklob.arena.domain.market.MarketErrorCode
import team.cklob.arena.domain.market.domain.entity.Symbol
import team.cklob.arena.domain.market.domain.repository.SymbolRepository
import team.cklob.arena.domain.market.domain.type.MarketType
import team.cklob.arena.domain.market.infrastructure.property.MarketProperties
import team.cklob.arena.global.exception.ExpectedException

@Component
class MarketValidator(
private val symbolRepository: SymbolRepository,
private val marketProperties: MarketProperties,
) {
fun requireActiveMarket(market: MarketType) {
if (market !in marketProperties.activeMarkets) throw ExpectedException(MarketErrorCode.MARKET_NOT_ACTIVE)
}

fun findActiveSymbol(symbolId: Long): Symbol {
val symbol = symbolRepository.findById(symbolId).orElseThrow { ExpectedException(MarketErrorCode.SYMBOL_NOT_FOUND) }
requireActiveMarket(symbol.market)
if (!symbol.isActive) throw ExpectedException(MarketErrorCode.SYMBOL_NOT_ACTIVE)
return symbol
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package team.cklob.arena.domain.market.application

import team.cklob.arena.domain.market.application.result.SymbolPageResult
import team.cklob.arena.domain.market.domain.type.MarketType

interface SearchSymbolsService {
fun execute(
market: MarketType,
keyword: String,
page: Int,
size: Int,
): SymbolPageResult
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package team.cklob.arena.domain.market.application

interface SyncSymbolsService {
fun execute()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package team.cklob.arena.domain.market.application.impl

import org.springframework.stereotype.Service
import team.cklob.arena.domain.market.application.GetCurrentPriceService
import team.cklob.arena.domain.market.application.MarketDataCache
import team.cklob.arena.domain.market.application.MarketDataClient
import team.cklob.arena.domain.market.application.MarketValidator
import team.cklob.arena.domain.market.application.result.CurrentPriceResult

@Service
class GetCurrentPriceServiceImpl(
private val marketValidator: MarketValidator,
private val marketDataCache: MarketDataCache,
private val marketDataClient: MarketDataClient,
) : GetCurrentPriceService {
override fun execute(symbolId: Long): CurrentPriceResult {
val symbol = marketValidator.findActiveSymbol(symbolId)

return marketDataCache.findCurrentPrice(symbolId) ?: marketDataClient.fetchCurrentPrice(symbol.code).let {
Comment thread
exijn marked this conversation as resolved.
CurrentPriceResult(symbolId, it.price, it.snapshotAt).also { result ->
marketDataCache.saveCurrentPrice(symbol.market, result)
}
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package team.cklob.arena.domain.market.application.impl

import org.springframework.stereotype.Service
import team.cklob.arena.domain.market.MarketErrorCode
import team.cklob.arena.domain.market.application.GetPriceHistoryService
import team.cklob.arena.domain.market.application.MarketDataCache
import team.cklob.arena.domain.market.application.MarketDataClient
import team.cklob.arena.domain.market.application.MarketValidator
import team.cklob.arena.domain.market.application.result.PriceHistoryResult
import team.cklob.arena.domain.market.infrastructure.property.MarketProperties
import team.cklob.arena.global.exception.ExpectedException
import java.time.LocalDate
import java.time.temporal.ChronoUnit

@Service
class GetPriceHistoryServiceImpl(
private val marketValidator: MarketValidator,
private val marketDataCache: MarketDataCache,
private val marketDataClient: MarketDataClient,
private val marketProperties: MarketProperties,
) : GetPriceHistoryService {
override fun execute(
symbolId: Long,
from: LocalDate,
to: LocalDate,
): PriceHistoryResult {
val days = ChronoUnit.DAYS.between(from, to) + 1
if (days !in 1..marketProperties.historyMaxDays) {
throw ExpectedException(MarketErrorCode.INVALID_PRICE_HISTORY_RANGE)
}
val symbol = marketValidator.findActiveSymbol(symbolId)

return marketDataCache.findPriceHistory(symbolId, from.toString(), to.toString())
?: PriceHistoryResult(
symbolId = symbolId,
from = from.toString(),
to = to.toString(),
prices = marketDataClient.fetchPriceHistory(symbol.code, from, to).sortedBy { it.snapshotAt },
Comment thread
exijn marked this conversation as resolved.
).also(marketDataCache::savePriceHistory)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
package team.cklob.arena.domain.market.application.impl

import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import team.cklob.arena.domain.market.application.GetSymbolsService
import team.cklob.arena.domain.market.application.MarketValidator
import team.cklob.arena.domain.market.application.result.SymbolPageResult
import team.cklob.arena.domain.market.domain.repository.SymbolRepository
import team.cklob.arena.domain.market.domain.type.MarketType

@Service
class GetSymbolsServiceImpl(
private val symbolRepository: SymbolRepository,
private val marketValidator: MarketValidator,
) : GetSymbolsService {
@Transactional(readOnly = true)
override fun execute(
market: MarketType,
page: Int,
size: Int,
): SymbolPageResult {
marketValidator.requireActiveMarket(market)
val pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, "code"))
return SymbolPageResult.from(symbolRepository.findAllByMarketAndIsActiveTrue(market, pageable))
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package team.cklob.arena.domain.market.application.impl

import org.springframework.data.domain.PageRequest
import org.springframework.data.domain.Sort
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional
import team.cklob.arena.domain.market.application.MarketValidator
import team.cklob.arena.domain.market.application.SearchSymbolsService
import team.cklob.arena.domain.market.application.result.SymbolPageResult
import team.cklob.arena.domain.market.domain.repository.SymbolRepository
import team.cklob.arena.domain.market.domain.type.MarketType
import team.cklob.arena.global.exception.CommonErrorCode
import team.cklob.arena.global.exception.ExpectedException

@Service
class SearchSymbolsServiceImpl(
private val symbolRepository: SymbolRepository,
private val marketValidator: MarketValidator,
) : SearchSymbolsService {
@Transactional(readOnly = true)
override fun execute(
market: MarketType,
keyword: String,
page: Int,
size: Int,
): SymbolPageResult {
marketValidator.requireActiveMarket(market)
val normalizedKeyword =
keyword.trim().takeIf { it.isNotEmpty() && it.length <= 100 }
?: throw ExpectedException(CommonErrorCode.INVALID_REQUEST)
val pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.ASC, "code"))
return SymbolPageResult.from(symbolRepository.searchActive(market, normalizedKeyword.escapeLikePattern(), pageable))
}

private fun String.escapeLikePattern(): String =
replace("\\", "\\\\")
.replace("%", "\\%")
.replace("_", "\\_")
}
Loading
Loading