Skip to content
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

[TNT-180] 트레이니 주간 캘린더 연동 #67

Merged
merged 9 commits into from
Feb 8, 2025
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package co.kr.tnt.designsystem.component.calendar.utils

import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.snapshotFlow
import com.kizitonwose.calendar.compose.CalendarLayoutInfo
import com.kizitonwose.calendar.compose.CalendarState
import com.kizitonwose.calendar.compose.weekcalendar.WeekCalendarState
import com.kizitonwose.calendar.core.CalendarMonth
import kotlinx.coroutines.flow.filterNotNull
import java.time.LocalDate
import java.time.YearMonth

/**
* 주어진 [viewportPercent] 를 기준으로, 현재 화면상으로 가장 많이 보이는 '월' 을 반환합니다.
*/
@Composable
fun rememberMostVisibleMonth(
state: CalendarState,
viewportPercent: Float = 90f,
): YearMonth {
val visibleMonth = remember(state) { mutableStateOf(state.firstVisibleMonth) }
LaunchedEffect(state) {
snapshotFlow { state.layoutInfo.firstMostVisibleMonth(viewportPercent) }
.filterNotNull()
.collect { month -> visibleMonth.value = month }
}
return visibleMonth.value.yearMonth
}

private fun CalendarLayoutInfo.firstMostVisibleMonth(viewportPercent: Float = 50f): CalendarMonth? {
return if (visibleMonthsInfo.isEmpty()) {
null
} else {
val viewportSize = (viewportEndOffset + viewportStartOffset) * viewportPercent / 100f
visibleMonthsInfo.firstOrNull { itemInfo ->
if (itemInfo.offset < 0) {
itemInfo.offset + itemInfo.size >= viewportSize
} else {
itemInfo.size - itemInfo.offset >= viewportSize
}
}?.month
}
}

/**
* 화면에 보여지는 7일 중 과반 이상 (4일) 보여지는 '월' 을 반환합니다.
*/
@Composable
fun rememberMostVisibleYearMonth(
state: WeekCalendarState,
): YearMonth {
val visibleYearMonth = remember { mutableStateOf(YearMonth.now()) }

LaunchedEffect(state) {
snapshotFlow { state.firstVisibleWeek.days.map { it.date } }
.collect { visibleDays ->
val dominantYearMonth = getDominantYearMonth(visibleDays)
visibleYearMonth.value = dominantYearMonth
}
}

return visibleYearMonth.value
}

private fun getDominantYearMonth(visibleDays: List<LocalDate>): YearMonth {
val yearMonthCount = visibleDays.groupingBy { YearMonth.from(it) }.eachCount()

return yearMonthCount.entries
.sortedByDescending { it.value }
.firstOrNull { it.value >= 4 }?.key
?: YearMonth.from(visibleDays.first())
}
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private fun getDominantYearMonth(visibleDays: List<LocalDate>): YearMonth {
val yearMonthCount = visibleDays.groupingBy { YearMonth.from(it) }.eachCount()
return yearMonthCount.entries
.sortedByDescending { it.value }
.firstOrNull { it.value >= 4 }?.key
?: YearMonth.from(visibleDays.first())
}
private fun getMostFrequentYearMonth(visibleDays: List<LocalDate>): YearMonth {
return visibleDays
.groupingBy { YearMonth.from(it) }
.eachCount()
.maxBy { it.value }
.key
}

요거 단순하게 Grouping 한 후에 값이 가장 많은 YearMonth 로 넘겨주는건 어떨까요?

캘린더 상에서 과반이 항상 4일 이상을 의미하는건 아닐 것 같아요!

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

옹 넵 수정하고 머지하겠습니다!! 🙇

18 changes: 18 additions & 0 deletions core/designsystem/src/main/res/drawable/ic_alarm.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
<vector xmlns:android="http://schemas.android.com/apk/res/android"
android:width="24dp"
android:height="24dp"
android:viewportWidth="24"
android:viewportHeight="24">
<path
android:pathData="M5.202,17.5H18.798C19.844,17.5 20.551,16.434 20.146,15.471L19.701,14.415C19.238,13.316 19,12.136 19,10.944V9C19,5.134 15.866,2 12,2C8.134,2 5,5.134 5,9V10.944C5,12.136 4.762,13.316 4.299,14.415L3.854,15.471C3.449,16.434 4.156,17.5 5.202,17.5Z"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#262626"/>
<path
android:pathData="M9,18C9,18 9,21 12,21C15,21 15,18 15,18"
android:strokeLineJoin="round"
android:strokeWidth="2"
android:fillColor="#00000000"
android:strokeColor="#262626"
android:strokeLineCap="round"/>
</vector>
74 changes: 74 additions & 0 deletions core/ui/src/main/java/co/kr/tnt/ui/component/TnTHomeTopBar.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package co.kr.tnt.ui.component

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.material3.CenterAlignedTopAppBar
import androidx.compose.material3.ExperimentalMaterial3Api
import androidx.compose.material3.Icon
import androidx.compose.material3.IconButton
import androidx.compose.material3.TopAppBarDefaults
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import co.kr.tnt.core.designsystem.R
import co.kr.tnt.designsystem.component.calendar.TnTCalendarSelector
import co.kr.tnt.designsystem.theme.TnTTheme
import java.time.YearMonth

@OptIn(ExperimentalMaterial3Api::class)
@Composable
fun TnTHomeTopBar(
modifier: Modifier = Modifier,
yearMonth: YearMonth = YearMonth.now(),
onClickSelectorPrevious: () -> Unit = { },
onClickSelectorNext: () -> Unit = { },
onClickNotification: () -> Unit = { },
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
) {
Column {
CenterAlignedTopAppBar(
modifier = modifier
.fillMaxWidth()
.padding(
vertical = 8.dp,
horizontal = 16.dp,
),
title = {
TnTCalendarSelector(
yearMonth = yearMonth,
onClickPrevious = onClickSelectorPrevious,
onClickNext = onClickSelectorNext,
)
},
actions = {
IconButton(
onClick = onClickNotification,
modifier = Modifier.size(32.dp),
) {
Icon(
painter = painterResource(R.drawable.ic_alarm),
contentDescription = "Go to notification screen",
)
}
},
colors = TopAppBarDefaults.centerAlignedTopAppBarColors(
containerColor = TnTTheme.colors.commonColors.Common0,
),
windowInsets = windowInsets,
expandedHeight = 48.dp,
)
}
}

@Preview(showBackground = true)
@Composable
private fun TnTHomeTopBarPreview() {
TnTTheme {
TnTHomeTopBar()
}
}
12 changes: 12 additions & 0 deletions domain/src/main/java/co/kr/tnt/domain/model/RecordList.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package co.kr.tnt.domain.model

import java.time.LocalDate

data class RecordList(
val recordDate: LocalDate,
val recordType: RecordType,
val recordTime: String,
val recordImage: String?,
val recordContents: String,
val hasFeedback: Boolean,
)
1 change: 1 addition & 0 deletions feature/trainee/home/build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ android {

dependencies {
implementation(libs.kotlinx.immutable)
implementation(libs.calendar.compose)
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
package co.kr.tnt.trainee.home

import co.kr.tnt.domain.model.RecordList
import co.kr.tnt.ui.base.UiEvent
import co.kr.tnt.ui.base.UiSideEffect
import co.kr.tnt.ui.base.UiState
import java.time.LocalDate

class TraineeHomeContract {
data class TraineeHomeUiState(
val selectedDate: LocalDate = LocalDate.now(),
val markedDates: List<LocalDate> = emptyList(),
val recordList: List<RecordList> = emptyList(),
) : UiState

sealed interface TraineeHomeUiEvent : UiEvent {
data object OnClickNextWeek : TraineeHomeUiEvent
data object OnClickPreviousWeek : TraineeHomeUiEvent
data class OnClickDay(val date: LocalDate) : TraineeHomeUiEvent
}

sealed interface TraineeHomeEffect : UiSideEffect {
data class ShowToast(val message: String) : TraineeHomeEffect
}
}
Original file line number Diff line number Diff line change
@@ -1,37 +1,143 @@
package co.kr.tnt.trainee.home

import androidx.compose.foundation.layout.Column
import android.widget.Toast
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Button
import androidx.compose.foundation.lazy.LazyColumn
import androidx.compose.material3.Scaffold
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.getValue
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Modifier
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import androidx.hilt.navigation.compose.hiltViewModel
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import co.kr.tnt.designsystem.component.calendar.TnTIndicatorWeekCalendar
import co.kr.tnt.designsystem.component.calendar.model.DayIndicatorState
import co.kr.tnt.designsystem.component.calendar.model.DayState
import co.kr.tnt.designsystem.component.calendar.utils.rememberMostVisibleYearMonth
import co.kr.tnt.designsystem.theme.TnTTheme
import co.kr.tnt.trainee.home.TraineeHomeContract.TraineeHomeUiEvent
import co.kr.tnt.trainee.home.TraineeHomeContract.TraineeHomeUiState
import co.kr.tnt.ui.component.TnTHomeTopBar
import com.kizitonwose.calendar.compose.weekcalendar.rememberWeekCalendarState
import kotlinx.coroutines.launch
import java.time.DayOfWeek
import java.time.LocalDate

@Composable
@Suppress("UnusedParameter")
internal fun TraineeHomeRoute(
viewModel: TraineeHomeViewModel = hiltViewModel(),
navigateToNotification: () -> Unit,
) {
TraineeHomeScreen(navigateToNotification)
val context = LocalContext.current
val uiState by viewModel.uiState.collectAsStateWithLifecycle()

TraineeHomeScreen(
state = uiState,
onClickNotification = navigateToNotification,
onSelectDate = { date ->
viewModel.setEvent(TraineeHomeUiEvent.OnClickDay(date))
},
onClickNextWeek = { viewModel.setEvent(TraineeHomeUiEvent.OnClickNextWeek) },
onClickPreviousWeek = { viewModel.setEvent(TraineeHomeUiEvent.OnClickPreviousWeek) },
)

LaunchedEffect(viewModel.effect) {
viewModel.effect.collect { effect ->
when (effect) {
is TraineeHomeContract.TraineeHomeEffect.ShowToast -> {
Toast.makeText(context, effect.message, Toast.LENGTH_SHORT).show()
}
}
}
}
}

@Composable
fun TraineeHomeScreen(
navigateToNotification: () -> Unit,
private fun TraineeHomeScreen(
state: TraineeHomeUiState,
onSelectDate: (LocalDate) -> Unit,
onClickNextWeek: () -> Unit,
onClickPreviousWeek: () -> Unit,
onClickNotification: () -> Unit,
) {
Scaffold(modifier = Modifier.fillMaxSize()) { innerPadding ->
Column {
Text(
text = "trainee home",
modifier = Modifier.padding(innerPadding),
)
Button(onClick = navigateToNotification) {
Text("navigate to notification")
val now = LocalDate.now()
val coroutineScope = rememberCoroutineScope()

val weekCalendarState = rememberWeekCalendarState(
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

우리 앱의 첫번째 요일은 '일요일'이므로 별도로 FirstDayOfWeek 선언해주는게 좋을 것 같습니다 ~

firstDayOfWeek = DayOfWeek.SUNDAY,
firstVisibleWeekDate = state.selectedDate,
startDate = now.minusYears(10),
endDate = now.plusYears(10),
)

val visibleYearMonth = rememberMostVisibleYearMonth(weekCalendarState)

Scaffold(
containerColor = TnTTheme.colors.commonColors.Common0,
modifier = Modifier.fillMaxSize(),
) { innerPadding ->
LazyColumn(modifier = Modifier.padding(innerPadding)) {
item {
Spacer(modifier = Modifier.height(12.dp))
TnTHomeTopBar(
yearMonth = visibleYearMonth,
onClickSelectorPrevious = {
coroutineScope.launch {
onClickPreviousWeek()
weekCalendarState.animateScrollToWeek(state.selectedDate)
}
},
onClickSelectorNext = {
coroutineScope.launch {
onClickNextWeek()
weekCalendarState.animateScrollToWeek(state.selectedDate)
}
},
onClickNotification = onClickNotification,
)
Spacer(modifier = Modifier.height(16.dp))
TnTIndicatorWeekCalendar(
state = weekCalendarState,
dayState = { date ->
DayState(isSelected = date == state.selectedDate)
},
indicatorState = { date ->
DayIndicatorState(showIcon = date in state.markedDates)
},
onClickDay = { date ->
onSelectDate(date)
},
)
Spacer(modifier = Modifier.height(12.dp))
}
}
}
}

@Preview
@Composable
private fun TraineeHomeScreenPreview() {
val now = LocalDate.now()

val dummyUiState = TraineeHomeUiState(
selectedDate = now,
markedDates = List(5) { now.minusDays(it.toLong() * 2) },
)

TnTTheme {
TraineeHomeScreen(
state = dummyUiState,
onClickNotification = { },
onSelectDate = {},
onClickNextWeek = { },
onClickPreviousWeek = { },
)
}
}
Loading