-
Notifications
You must be signed in to change notification settings - Fork 1
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
[CT-2-2-3] 차트게임 기능 - 실행 테스트 및 버그 수정 #35
Changes from 25 commits
85cada9
3e0e04c
0573512
012f3cd
c9f5535
c0c9c88
a922675
568e9ac
d3cadd8
d2f060d
17809f7
068198d
d3ccf97
afd02be
c949dc7
c123aab
af50e7c
c6d1c4d
8568076
95391ad
2c69b2a
5fa51fd
4310b54
c47db0b
9019b5a
7ee037b
59d2c0f
c1231e7
8a07b8e
3b02a4f
d610fd1
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ import com.yessorae.data.source.network.polygon.model.chart.asDomainModel | |
import com.yessorae.domain.common.ChartRequestArgumentHelper | ||
import com.yessorae.domain.entity.Chart | ||
import com.yessorae.domain.entity.tick.TickUnit | ||
import com.yessorae.domain.exception.ChartGameException | ||
import com.yessorae.domain.repository.ChartRepository | ||
import javax.inject.Inject | ||
import kotlinx.coroutines.CoroutineDispatcher | ||
|
@@ -23,19 +24,48 @@ class ChartRepositoryImpl @Inject constructor( | |
@Dispatcher(ChartTrainerDispatcher.IO) | ||
private val dispatcher: CoroutineDispatcher | ||
) : ChartRepository { | ||
override suspend fun fetchNewChartRandomly(): Chart = | ||
override suspend fun fetchNewChartRandomly(totalTurn: Int): Chart = | ||
withContext(dispatcher) { | ||
val chart = networkDataSource | ||
.getChart( | ||
ticker = chartRequestArgumentHelper.getRandomTicker(), | ||
tickUnit = appPreferences.getTickUnit(), | ||
from = chartRequestArgumentHelper.getFromDate(), | ||
to = chartRequestArgumentHelper.getToDate() | ||
) | ||
.asDomainModel(TickUnit.DAY) | ||
|
||
val chartId = localDBDataSource.insertChart(chart.asEntity()) | ||
localDBDataSource.insertTicks(chart.ticks.map { it.asEntity(chartId = chartId) }) | ||
chart.copy(id = chartId) | ||
fetchNewChartRandomlyWithRetry( | ||
currentRetryCount = 0, | ||
totalTurn = totalTurn | ||
) | ||
} | ||
|
||
private suspend fun fetchNewChartRandomlyWithRetry( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q; 이 역할을 가진 함수를 분리한 이유가 있나요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. fetchNewChartRandomly 함수를 사용하는 입장에서 알 필요 없는 currentRetryCount 같은 파라미터를 노출하지 않고 withContext 로 디스패칭을 바꿔주는 것을 한 번만 할 수 있어서 분리했습니다. |
||
currentRetryCount: Int, | ||
totalTurn: Int | ||
): Chart { | ||
// RETRY_COUNT 만큼 시도했는데도 실패하면 IllegalStateException 발생. | ||
// 거의 발생하지 않아도 안전망 역할 | ||
if (currentRetryCount > RETRY_COUNT) { | ||
throw ChartGameException.HardToFetchTradeException | ||
} | ||
|
||
val dto = networkDataSource | ||
.getChart( | ||
ticker = chartRequestArgumentHelper.getRandomTicker(), | ||
tickUnit = appPreferences.getTickUnit(), | ||
from = chartRequestArgumentHelper.getFromDate(), | ||
to = chartRequestArgumentHelper.getToDate() | ||
) | ||
|
||
// 서버에서 가져온 차트가 totalTurn 보다 작으면 다시 요청 | ||
if ((dto.ticks?.size ?: 0) < totalTurn) { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit; 옵셔널을 바로 해제해서 푸는 것보다 위에서 ticks가 널일 때 핸들링 하는 코드를 넣는게 어떨까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 두 번째 방법이 좋다고 생각합니다. 불필요한 null 핸들링이라고 생각합니다. 감사합니다! |
||
return fetchNewChartRandomlyWithRetry( | ||
currentRetryCount = currentRetryCount + 1, | ||
totalTurn = totalTurn | ||
) | ||
} | ||
|
||
val chart = dto.asDomainModel(TickUnit.DAY) | ||
|
||
val chartId = localDBDataSource.insertChart(chart.asEntity()) | ||
localDBDataSource.insertTicks(chart.ticks.map { it.asEntity(chartId = chartId) }) | ||
return chart.copy(id = chartId) | ||
} | ||
|
||
companion object { | ||
private const val RETRY_COUNT = 3 | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,29 +21,50 @@ data class ChartGame( | |
// 유저의 게임 강제 종료 여부 | ||
val isQuit: Boolean | ||
) { | ||
val totalProfit: Money = currentBalance - startBalance | ||
|
||
val rateOfProfit: Double = (totalProfit / startBalance).value * 100 | ||
|
||
val tradeCount: Int | ||
get() = trades.size | ||
|
||
val totalCommission: Money = Money(trades.sumOf { trade -> trade.commission.value }) | ||
|
||
val visibleTicks: List<Tick> = | ||
chart.ticks | ||
.sortedBy { it.startTimestamp } | ||
.subList(0, chart.ticks.size - totalTurn + currentTurn - 1) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Q; subList의 인덱스가 항상 범위 내에서 동작할까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 결론부터 말씀드리자면, 여기서도 범위 핸들링을하는 게 좋겠습니다. (보여줄 수 있는 만큼만 보여주기) 물론 현재 브랜치부터는 Tick 개수가 totalTurn 보다 작으면 에러를 내리게 해서 동작합니다. 하지만 생각해보니 애초에 N번 재시도 하고 그래도 Tick 개수가 totalTurn 보다 적으면 에러 내리는 방식이 좋은 UX는 아닌 것 같네요 그래서 해결방법을 두 가지 생각해봤는데요
결론은 두 가지 다 적용하자 입니다. 운영으로 풀고 혹여나 실수가 일어나도 무작정 에러를 내려주는 일은 막자는 생각입니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 운영적인 것은 천천히 적용하고 2번을 먼저 적용했습니다. |
||
|
||
val ownedStockCount = trades.sumOf { trade -> trade.count } | ||
val ownedStockCount = trades.sumOf { trade -> | ||
if (trade.type.isBuy()) { | ||
trade.count | ||
} else { | ||
-trade.count | ||
} | ||
} | ||
|
||
private val ownedTotalStockPrice = trades.sumOf { trade -> | ||
if (trade.type.isBuy()) { | ||
trade.totalTradeMoney.value | ||
} else { | ||
-(trade.totalTradeMoney.value) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit; 불필요한 소괄호인 것 같습니다. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 앗 제거 하겠습니다. 감사합니다. |
||
} | ||
} | ||
|
||
val ownedAverageStockPrice = if (ownedStockCount != 0) { | ||
Money(ownedTotalStockPrice / ownedStockCount) | ||
} else { | ||
Money(0.0) | ||
} | ||
|
||
private val ownedTotalStockPrice = trades.sumOf { trade -> trade.totalTradeMoney.value } | ||
private val currentClosePrice: Money = (visibleTicks.lastOrNull()?.closePrice ?: Money(0.0)) | ||
|
||
val ownedAverageStockPrice = Money(ownedTotalStockPrice / ownedStockCount) | ||
val totalProfit: Money = if (ownedStockCount != 0) { | ||
currentClosePrice - ownedAverageStockPrice | ||
} else { | ||
Money(0.0) | ||
} | ||
|
||
val rateOfProfit: Double = if (ownedStockCount != 0) { | ||
(totalProfit / ownedAverageStockPrice).value * 100 | ||
} else { | ||
0.0 | ||
} | ||
|
||
val currentStockPrice: Money = visibleTicks.lastOrNull()?.closePrice ?: Money(0.0) | ||
|
||
val currentGameProgress: Float = currentTurn / totalTurn.toFloat() * 100f | ||
val currentGameProgress: Float = currentTurn / totalTurn.toFloat() | ||
|
||
// 게임 모든 턴을 끝까지 완료한 경우 true | ||
val isGameComplete: Boolean = currentTurn == totalTurn | ||
|
@@ -70,7 +91,7 @@ data class ChartGame( | |
internal fun copyFrom(newTrade: Trade): ChartGame { | ||
return copy( | ||
trades = trades + newTrade, | ||
currentBalance = currentBalance + newTrade.profit | ||
currentBalance = currentBalance - newTrade.totalTradeMoney | ||
) | ||
} | ||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -21,46 +21,24 @@ import com.yessorae.presentation.ui.chartgame.component.ChartGameTopAppBarUi | |
import com.yessorae.presentation.ui.chartgame.component.order.TradeOrderUi | ||
import com.yessorae.presentation.ui.chartgame.model.ChartGameEvent | ||
import com.yessorae.presentation.ui.chartgame.model.ChartGameScreenUserAction | ||
import com.yessorae.presentation.ui.designsystem.component.ChartTrainerLoadingProgressBar | ||
import com.yessorae.presentation.ui.designsystem.util.showToast | ||
import kotlinx.coroutines.flow.SharedFlow | ||
import kotlinx.coroutines.flow.collectLatest | ||
|
||
@Composable | ||
fun ChartGameScreen(viewModel: ChartGameViewModel = viewModel()) { | ||
val state by viewModel.screenState.collectAsState() | ||
val context = LocalContext.current | ||
|
||
LaunchedEffect(key1 = Unit) { | ||
viewModel.screenEvent.collectLatest { event -> | ||
when (event) { | ||
is ChartGameEvent.InputBuyingStockCount -> { | ||
context.showToast(context.getString(R.string.chart_game_toast_trade_buy)) | ||
} | ||
|
||
is ChartGameEvent.InputSellingStockCount -> { | ||
context.showToast(context.getString(R.string.chart_game_toast_trade_sell)) | ||
} | ||
|
||
is ChartGameEvent.TradeFail -> { | ||
context.showToast(context.getString(R.string.chart_game_toast_trade_fail)) | ||
} | ||
|
||
is ChartGameEvent.MoveToBack -> { | ||
// TODO::LATER #23-navigation 셋업과 함께 추가될 내용 | ||
} | ||
ChartGameEventHandler(screenEvent = viewModel.screenEvent) | ||
|
||
is ChartGameEvent.MoveToTradeHistory -> { | ||
// TODO::LATER #6-트레이드 내역 확인 기능과 #23-navigation 셋업과 함께 추가될 내용 | ||
} | ||
} | ||
} | ||
} | ||
val state by viewModel.screenState.collectAsState() | ||
|
||
Scaffold( | ||
topBar = { | ||
ChartGameTopAppBarUi( | ||
isStart = state.isStart, | ||
isBeforeStart = state.isBeforeStart, | ||
totalProfit = state.totalProfit, | ||
totalRateOfProfit = state.rateOfProfit, | ||
enableChangeChartButton = state.enableChangeChartButton, | ||
onClickNewChartButton = { | ||
state.onUserAction(ChartGameScreenUserAction.ClickNewChartButton) | ||
}, | ||
|
@@ -109,14 +87,58 @@ fun ChartGameScreen(viewModel: ChartGameViewModel = viewModel()) { | |
) | ||
} | ||
|
||
TradeOrderUi( | ||
modifier = Modifier.fillMaxWidth(), | ||
tradeOrderUi = state.tradeOrderUi | ||
TradeOrderUi(tradeOrderUi = state.tradeOrderUi) | ||
|
||
ChartTrainerLoadingProgressBar( | ||
modifier = Modifier.fillMaxSize(), | ||
show = state.showLoading | ||
) | ||
} | ||
} | ||
} | ||
|
||
@Composable | ||
private fun ChartGameEventHandler(screenEvent: SharedFlow<ChartGameEvent>) { | ||
val context = LocalContext.current | ||
LaunchedEffect(key1 = Unit) { | ||
screenEvent.collectLatest { event -> | ||
when (event) { | ||
is ChartGameEvent.InputBuyingStockCount -> { | ||
context.showToast(context.getString(R.string.chart_game_toast_trade_buy)) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. nit; 코드의 간결함을 위해 반복되는 코드는 확장함수등으로 만들어 보는 것은 어떨까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 넵 그게 좋겠습니다. 감사합니다! |
||
} | ||
|
||
is ChartGameEvent.InputSellingStockCount -> { | ||
context.showToast(context.getString(R.string.chart_game_toast_trade_sell)) | ||
} | ||
|
||
is ChartGameEvent.TradeFail -> { | ||
context.showToast(context.getString(R.string.chart_game_toast_trade_fail)) | ||
} | ||
|
||
is ChartGameEvent.HardToFetchTrade -> { | ||
// TODO::LATER #5-유저(익명) 정보 확인/수정 기능이 추가되면 Screen 뒤로가기 추가 | ||
context.showToast( | ||
context.getString(R.string.chart_game_toast_hard_to_fetch_trade) | ||
) | ||
} | ||
|
||
is ChartGameEvent.GameHasEnded -> { | ||
// TODO::LATER #5-유저(익명) 정보 확인/수정 기능이 추가되면 Screen 뒤로가기로 변경 | ||
context.showToast(context.getString(R.string.chart_game_toast_game_has_ended)) | ||
} | ||
|
||
is ChartGameEvent.MoveToBack -> { | ||
// TODO::LATER #23-navigation 셋업과 함께 추가될 내용 | ||
} | ||
|
||
is ChartGameEvent.MoveToTradeHistory -> { | ||
// TODO::LATER #6-트레이드 내역 확인 기능과 #23-navigation 셋업과 함께 추가될 내용 | ||
} | ||
} | ||
} | ||
} | ||
} | ||
|
||
@Preview | ||
@Composable | ||
fun ChartGameScreenPreview() { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
설마 S&P500에 1년미만인 종목이 있을까 하며 처리 안하고 넘어갔던 부분인데, TotalTurn 보다 틱수가 낮으면 게임이 불가하여 TotalTurn 보다는 큰 게 나올 때까지 시도하되 무한 루프를 돌면 안되니 최대 횟수를 두었습니다.