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-178] 트레이너 홈화면 캘린더 구현 #68

Merged
merged 12 commits into from
Feb 8, 2025
Merged
Show file tree
Hide file tree
Changes from 11 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
@@ -1,9 +1,13 @@
package co.kr.tnt.designsystem.component.calendar

import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.height
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Modifier
import androidx.compose.ui.tooling.preview.Preview
import androidx.compose.ui.unit.dp
import co.kr.tnt.designsystem.component.calendar.composable.CalendarCell
import co.kr.tnt.designsystem.component.calendar.composable.CalendarCellWithIndicator
import co.kr.tnt.designsystem.component.calendar.composable.WeekLabels
Expand All @@ -13,9 +17,11 @@ import co.kr.tnt.designsystem.theme.TnTTheme
import com.kizitonwose.calendar.compose.CalendarState
import com.kizitonwose.calendar.compose.HorizontalCalendar
import com.kizitonwose.calendar.compose.rememberCalendarState
import com.kizitonwose.calendar.core.CalendarDay
import com.kizitonwose.calendar.core.DayPosition
import com.kizitonwose.calendar.core.daysOfWeek
import com.kizitonwose.calendar.core.yearMonth
import java.time.DayOfWeek
import java.time.LocalDate

@Composable
Expand All @@ -35,11 +41,16 @@ fun TnTMonthCalendar(
},
dayContent = { day ->
if (day.position == DayPosition.MonthDate) {
CalendarCell(
date = day.date,
state = dayState(day.date),
onClick = { onClickDay?.invoke(day.date) },
)
Column {
if (isFirstWeekOfMonth(day, state.firstDayOfWeek).not()) {
Spacer(modifier = Modifier.height(12.dp))
}
CalendarCell(
date = day.date,
state = dayState(day.date),
onClick = { onClickDay?.invoke(day.date) },
)
}
}
},
)
Expand All @@ -63,17 +74,40 @@ fun TnTIndicatorMonthCalendar(
},
dayContent = { day ->
if (day.position == DayPosition.MonthDate) {
CalendarCellWithIndicator(
date = day.date,
dayState = dayState(day.date),
indicatorState = indicatorState(day.date),
onClick = { onClickDay?.invoke(day.date) },
)
Column {
if (isFirstWeekOfMonth(day, state.firstDayOfWeek).not()) {
Spacer(modifier = Modifier.height(12.dp))
}
CalendarCellWithIndicator(
date = day.date,
dayState = dayState(day.date),
indicatorState = indicatorState(day.date),
onClick = { onClickDay?.invoke(day.date) },
)
}
}
},
)
}

private fun isFirstWeekOfMonth(
day: CalendarDay,
firstDayOfWeek: DayOfWeek,
): Boolean {
val startOfMonth = day.date.withDayOfMonth(1)
val firstDayOfStartWeek = startOfMonth.minusDays(
(startOfMonth.dayOfWeek.value % firstDayOfWeek.value).toLong(),
)

val firstWeekDays = buildList<LocalDate> {
repeat(7) { day ->
add(firstDayOfStartWeek.plusDays(day.toLong()))
}
}

return firstWeekDays.contains(day.date)
}

@Preview(showBackground = true)
@Preview(showBackground = true, widthDp = 500)
@Composable
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.material3.Icon
import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.remember
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
import androidx.compose.ui.res.painterResource
Expand Down Expand Up @@ -94,13 +95,22 @@ private fun CalendarDay(
isSelected: Boolean,
modifier: Modifier = Modifier,
) {
val today = remember { LocalDate.now() }
val isToday = date == today

val backgroundColor = when {
isSelected -> TnTTheme.colors.neutralColors.Neutral900
isToday -> TnTTheme.colors.neutralColors.Neutral200
else -> null
}

Box(
modifier = modifier
.size(32.dp)
.then(
if (isSelected) {
if (backgroundColor != null) {
Modifier.background(
color = TnTTheme.colors.neutralColors.Neutral900,
color = backgroundColor,
shape = RoundedCornerShape(8.dp),
)
} else {
Expand Down Expand Up @@ -154,6 +164,19 @@ private fun CalendarIndicator(
@Preview(showBackground = true)
@Composable
private fun CalendarCellPreview() {
TnTTheme {
CalendarCell(
date = LocalDate.now().minusDays(1),
state = DayState(
isSelected = false,
),
)
}
}

@Preview(showBackground = true)
@Composable
private fun CalendarTodayCellPreview() {
TnTTheme {
CalendarCell(
date = LocalDate.now(),
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
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.core.CalendarMonth
import kotlinx.coroutines.flow.filterNotNull
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
}
}
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 = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
) {
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()
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package co.kr.data.network.model.trainer

import co.kr.tnt.domain.model.trainer.DailyPtSessionCount
import co.kr.tnt.domain.utils.DateFormatter
import kotlinx.serialization.Serializable

@Serializable
data class MonthlyPtSessionCountsResponse(
val calendarPtLessonCounts: List<PtSessionCountsResponse>,
)

@Serializable
data class PtSessionCountsResponse(
val date: String,
val count: Int,
)

fun PtSessionCountsResponse.toDomain(dateFormatter: DateFormatter) =
DailyPtSessionCount(
date = dateFormatter.parse(date),
count = count,
)
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import co.kr.data.network.model.LoginRequest
import co.kr.data.network.model.LoginResponse
import co.kr.data.network.model.SignUpResponse
import co.kr.data.network.model.VerifyCodeResponse
import co.kr.data.network.model.trainer.MonthlyPtSessionCountsResponse
import co.kr.data.network.util.WithoutSessionCheckPath.CHECK_SESSION_PATH
import okhttp3.MultipartBody
import okhttp3.RequestBody
Expand Down Expand Up @@ -61,4 +62,10 @@ interface ApiService {
@Query("trainerId") trainerId: String,
@Query("traineeId") traineeId: String,
): ConnectedTraineeResponse

@GET("/trainers/lessons/calendar")
suspend fun getMonthlyPtSessionCounts(
@Query("year") year: Int,
@Query("month") month: Int,
): MonthlyPtSessionCountsResponse
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package co.kr.data.network.source

import co.kr.data.network.model.trainer.MonthlyPtSessionCountsResponse
import co.kr.data.network.service.ApiService
import co.kr.data.network.util.networkHandler
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class TrainerRemoteDataSource @Inject constructor(
private val apiService: ApiService,
) {
suspend fun getMonthlyPtSessionCounts(
year: Int,
month: Int,
): MonthlyPtSessionCountsResponse = networkHandler {
apiService.getMonthlyPtSessionCounts(
year = year,
month = month,
)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,8 +8,10 @@ import co.kr.tnt.domain.model.ConnectedResult
import co.kr.tnt.domain.model.InviteCodeResult
import co.kr.tnt.domain.repository.ConnectRepository
import javax.inject.Inject
import javax.inject.Singleton

class ConnectRepositoryImpl @Inject constructor(
@Singleton
internal class ConnectRepositoryImpl @Inject constructor(
Comment on lines -12 to +14
Copy link
Contributor

Choose a reason for hiding this comment

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

억 감사합니다

private val connectRemoteDataSource: ConnectRemoteDataSource,
) : ConnectRepository {
override suspend fun getInviteCode(): InviteCodeResult {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,8 +17,10 @@ import okhttp3.RequestBody.Companion.asRequestBody
import okhttp3.RequestBody.Companion.toRequestBody
import java.io.File
import javax.inject.Inject
import javax.inject.Singleton

class SignUpRepositoryImpl @Inject constructor(
@Singleton
internal class SignUpRepositoryImpl @Inject constructor(
private val signupRemoteDataSource: SignUpRemoteDataSource,
private val sessionLocalDataSource: SessionLocalDataSource,
private val json: Json,
Expand Down
Loading