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 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
@@ -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
Expand Up @@ -28,7 +28,7 @@ fun TnTHomeTopBar(
onClickSelectorPrevious: () -> Unit = { },
onClickSelectorNext: () -> Unit = { },
onClickNotification: () -> Unit = { },
windowInsets: WindowInsets = TopAppBarDefaults.windowInsets,
windowInsets: WindowInsets = WindowInsets(0.dp, 0.dp, 0.dp, 0.dp),
) {
Column {
CenterAlignedTopAppBar(
Expand Down
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
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package co.kr.data.repository

import co.kr.data.network.model.trainer.toDomain
import co.kr.data.network.source.TrainerRemoteDataSource
import co.kr.tnt.domain.model.trainer.DailyPtSessionCount
import co.kr.tnt.domain.repository.TrainerRepository
import co.kr.tnt.domain.utils.DateFormatter
import java.time.YearMonth
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
internal class TrainerRepositoryImpl @Inject constructor(
private val trainerRemoteDataSource: TrainerRemoteDataSource,
private val dateFormatter: DateFormatter,
) : TrainerRepository {
override suspend fun getMonthlyPtSessionCounts(yearMonth: YearMonth): List<DailyPtSessionCount> =
trainerRemoteDataSource.getMonthlyPtSessionCounts(
year = yearMonth.year,
month = yearMonth.monthValue,
).calendarPtLessonCounts.map { response ->
response.toDomain(dateFormatter)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,11 @@ package co.kr.data.repository.di
import co.kr.data.repository.ConnectRepositoryImpl
import co.kr.data.repository.LoginRepositoryImpl
import co.kr.data.repository.SignUpRepositoryImpl
import co.kr.data.repository.TrainerRepositoryImpl
import co.kr.tnt.domain.repository.ConnectRepository
import co.kr.tnt.domain.repository.LoginRepository
import co.kr.tnt.domain.repository.SignUpRepository
import co.kr.tnt.domain.repository.TrainerRepository
import dagger.Binds
import dagger.Module
import dagger.hilt.InstallIn
Expand All @@ -28,4 +30,9 @@ internal abstract class RepositoryModule {
abstract fun bindConnectRepository(
repository: ConnectRepositoryImpl,
): ConnectRepository

@Binds
abstract fun bindTrainerRepository(
repository: TrainerRepositoryImpl,
): TrainerRepository
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package co.kr.tnt.domain.model.trainer

import java.time.LocalDate

data class DailyPtSessionCount(
val date: LocalDate,
val count: Int,
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package co.kr.tnt.domain.repository

import co.kr.tnt.domain.model.trainer.DailyPtSessionCount
import java.time.YearMonth

interface TrainerRepository {
suspend fun getMonthlyPtSessionCounts(yearMonth: YearMonth): List<DailyPtSessionCount>
}
27 changes: 27 additions & 0 deletions domain/src/main/java/co/kr/tnt/domain/utils/DateFormatter.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package co.kr.tnt.domain.utils

import java.time.LocalDate
import java.time.format.DateTimeFormatter
import javax.inject.Inject
import javax.inject.Singleton

@Singleton
class DateFormatter @Inject constructor() {
fun parse(
rawDate: String,
formatter: DateTimeFormatter = DEFAULT_DATE_FORMAT,
): LocalDate {
require(rawDate.isNotBlank())

return LocalDate.parse(rawDate, formatter)
}

fun format(
date: LocalDate,
formatter: DateTimeFormatter = DEFAULT_DATE_FORMAT,
): String = formatter.format(date)

companion object {
private val DEFAULT_DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd")
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import java.time.YearMonth
internal class TrainerHomeContract {
data class TrainerHomeUiState(
val selectedDay: LocalDate = LocalDate.now(),
val dailyPtSessionCount: Map<LocalDate, Int> = mapOf(),
) : UiState

sealed interface TrainerHomeUiEvent : UiEvent {
Expand All @@ -19,5 +20,6 @@ internal class TrainerHomeContract {

sealed interface TrainerHomeSideEffect : UiSideEffect {
data object NavigateToNotification : TrainerHomeSideEffect
data class ShowToast(val message: String) : TrainerHomeSideEffect
}
}
Loading