diff --git a/app/src/main/java/org/sopt/official/data/AttendanceMapper.kt b/app/src/main/java/org/sopt/official/data/AttendanceMapper.kt new file mode 100644 index 000000000..c5889ce67 --- /dev/null +++ b/app/src/main/java/org/sopt/official/data/AttendanceMapper.kt @@ -0,0 +1,84 @@ +package org.sopt.official.data + +import org.sopt.official.data.model.attendance.AttendanceHistoryResponse +import org.sopt.official.data.model.attendance.AttendanceHistoryResponse.AttendanceResponse +import org.sopt.official.data.model.attendance.SoptEventResponse +import org.sopt.official.domain.entity.attendance.Attendance +import org.sopt.official.domain.entity.attendance.Attendance.AttendanceDayType +import org.sopt.official.domain.entity.attendance.Attendance.AttendanceDayType.HasAttendance.RoundAttendance +import org.sopt.official.domain.entity.attendance.Attendance.AttendanceDayType.HasAttendance.RoundAttendance.RoundAttendanceState +import org.sopt.official.domain.entity.attendance.Attendance.Session +import org.sopt.official.domain.entity.attendance.Attendance.User.AttendanceLog.AttendanceState +import java.time.LocalDateTime + +fun mapToAttendance( + attendanceHistoryResponse: AttendanceHistoryResponse?, + soptEventResponse: SoptEventResponse? +): Attendance { + return Attendance( + sessionId = soptEventResponse?.id ?: Attendance.UNKNOWN_SESSION_ID, + user = Attendance.User( + name = attendanceHistoryResponse?.name ?: Attendance.User.UNKNOWN_NAME, + generation = attendanceHistoryResponse?.generation ?: Attendance.User.UNKNOWN_GENERATION, + part = Attendance.User.Part.valueOf(attendanceHistoryResponse?.part ?: Attendance.User.UNKNOWN_PART), + attendanceScore = attendanceHistoryResponse?.score ?: 0.0, + attendanceCount = Attendance.User.AttendanceCount( + attendanceCount = attendanceHistoryResponse?.attendanceCount?.normal ?: 0, + lateCount = attendanceHistoryResponse?.attendanceCount?.late ?: 0, + absenceCount = attendanceHistoryResponse?.attendanceCount?.abnormal ?: 0, + ), + attendanceHistory = attendanceHistoryResponse?.attendances?.map { attendanceResponse: AttendanceResponse -> + Attendance.User.AttendanceLog( + sessionName = attendanceResponse.eventName, + date = attendanceResponse.date, + attendanceState = AttendanceState.valueOf(attendanceResponse.attendanceState) + ) + } ?: emptyList(), + ), + attendanceDayType = soptEventResponse.toAttendanceDayType() + ) +} + +private fun SoptEventResponse?.toAttendanceDayType(): AttendanceDayType { + return when (this?.type) { + "HAS_ATTENDANCE" -> { + val firstAttendanceResponse: SoptEventResponse.AttendanceResponse? = attendances.getOrNull(0) + val secondAttendanceResponse: SoptEventResponse.AttendanceResponse? = attendances.getOrNull(1) + AttendanceDayType.HasAttendance( + session = Session( + name = eventName, + location = location.ifBlank { null }, + startAt = LocalDateTime.parse(startAt), + endAt = LocalDateTime.parse(endAt), + ), + firstRoundAttendance = RoundAttendance( + state = RoundAttendanceState.valueOf(firstAttendanceResponse?.status ?: RoundAttendanceState.NOT_YET.name), + attendedAt = LocalDateTime.parse(firstAttendanceResponse?.attendedAt), + ), + secondRoundAttendance = RoundAttendance( + state = RoundAttendanceState.valueOf(secondAttendanceResponse?.status ?: RoundAttendanceState.NOT_YET.name), + attendedAt = LocalDateTime.parse(secondAttendanceResponse?.attendedAt), + ), + ) + } + + "NO_ATTENDANCE" -> { + AttendanceDayType.NoAttendance( + session = Session( + name = eventName, + location = location.ifBlank { null }, + startAt = LocalDateTime.parse(startAt), + endAt = LocalDateTime.parse(endAt), + ) + ) + } + + "NO_SESSION" -> { + AttendanceDayType.NoSession + } + + else -> { + AttendanceDayType.NoSession + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/data/repository/attendance/DefaultAttendanceRepository.kt b/app/src/main/java/org/sopt/official/data/repository/attendance/DefaultAttendanceRepository.kt new file mode 100644 index 000000000..be4d3e1e3 --- /dev/null +++ b/app/src/main/java/org/sopt/official/data/repository/attendance/DefaultAttendanceRepository.kt @@ -0,0 +1,73 @@ +package org.sopt.official.data.repository.attendance + +import kotlinx.serialization.json.Json +import kotlinx.serialization.json.JsonObject +import kotlinx.serialization.json.contentOrNull +import kotlinx.serialization.json.jsonObject +import kotlinx.serialization.json.jsonPrimitive +import org.sopt.official.data.mapToAttendance +import org.sopt.official.data.model.attendance.AttendanceHistoryResponse +import org.sopt.official.data.model.attendance.AttendanceRoundResponse +import org.sopt.official.data.model.attendance.RequestAttendanceCode +import org.sopt.official.data.model.attendance.SoptEventResponse +import org.sopt.official.data.service.attendance.AttendanceService +import org.sopt.official.domain.entity.attendance.Attendance +import org.sopt.official.domain.entity.attendance.ConfirmAttendanceCodeResult +import org.sopt.official.domain.entity.attendance.FetchAttendanceCurrentRoundResult +import org.sopt.official.domain.repository.attendance.NewAttendanceRepository +import retrofit2.HttpException +import javax.inject.Inject + +class DefaultAttendanceRepository @Inject constructor( + private val attendanceService: AttendanceService, + private val json: Json +) : NewAttendanceRepository { + override suspend fun fetchAttendanceInfo(): Attendance { + val soptEventResponse: SoptEventResponse? = runCatching { attendanceService.getSoptEvent().data }.getOrNull() + val attendanceHistoryResponse: AttendanceHistoryResponse? = + runCatching { attendanceService.getAttendanceHistory().data }.getOrNull() + + val attendance: Attendance = + mapToAttendance(attendanceHistoryResponse = attendanceHistoryResponse, soptEventResponse = soptEventResponse) + return attendance + } + + override suspend fun fetchAttendanceCurrentRound(lectureId: Long): FetchAttendanceCurrentRoundResult { + return runCatching { attendanceService.getAttendanceRound(lectureId).data }.fold( + onSuccess = { attendanceRoundResponse: AttendanceRoundResponse? -> + FetchAttendanceCurrentRoundResult.Success(attendanceRoundResponse?.round) + }, + onFailure = { error: Throwable -> + if (error !is HttpException) return FetchAttendanceCurrentRoundResult.Failure(null) + + val message: String? = error.jsonErrorMessage + FetchAttendanceCurrentRoundResult.Failure(message) + }, + ) + } + + override suspend fun confirmAttendanceCode( + subLectureId: Long, + code: String + ): ConfirmAttendanceCodeResult { + return runCatching { + attendanceService.confirmAttendanceCode(RequestAttendanceCode(subLectureId = subLectureId, code = code)) + }.fold( + onSuccess = { ConfirmAttendanceCodeResult.Success }, + onFailure = { error: Throwable -> + if (error !is HttpException) return ConfirmAttendanceCodeResult.Failure(null) + + val message: String? = error.jsonErrorMessage + ConfirmAttendanceCodeResult.Failure(message) + }, + ) + } + + private val HttpException.jsonErrorMessage: String? + get() { + val errorBody: String = this.response()?.errorBody()?.string() ?: return null + val jsonObject: JsonObject = json.parseToJsonElement(errorBody).jsonObject + val errorMessage: String? = jsonObject["message"]?.jsonPrimitive?.contentOrNull + return errorMessage + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/di/attendance/AttendanceBindsModule.kt b/app/src/main/java/org/sopt/official/di/attendance/AttendanceBindsModule.kt index bd754a504..cbce18ed1 100644 --- a/app/src/main/java/org/sopt/official/di/attendance/AttendanceBindsModule.kt +++ b/app/src/main/java/org/sopt/official/di/attendance/AttendanceBindsModule.kt @@ -29,12 +29,14 @@ import dagger.Module import dagger.Provides import dagger.hilt.InstallIn import dagger.hilt.components.SingletonComponent -import javax.inject.Singleton import org.sopt.official.common.di.OperationRetrofit import org.sopt.official.data.repository.attendance.AttendanceRepositoryImpl +import org.sopt.official.data.repository.attendance.DefaultAttendanceRepository import org.sopt.official.data.service.attendance.AttendanceService import org.sopt.official.domain.repository.attendance.AttendanceRepository +import org.sopt.official.domain.repository.attendance.NewAttendanceRepository import retrofit2.Retrofit +import javax.inject.Singleton @Module @InstallIn(SingletonComponent::class) @@ -43,6 +45,10 @@ abstract class AttendanceBindsModule { @Singleton abstract fun bindAttendanceRepository(attendanceRepositoryImpl: AttendanceRepositoryImpl): AttendanceRepository + @Binds + @Singleton + abstract fun bindDefaultAttendanceRepository(defaultAttendanceRepository: DefaultAttendanceRepository): NewAttendanceRepository + companion object { @Provides @Singleton diff --git a/app/src/main/java/org/sopt/official/domain/entity/attendance/Attendance.kt b/app/src/main/java/org/sopt/official/domain/entity/attendance/Attendance.kt new file mode 100644 index 000000000..2557529e4 --- /dev/null +++ b/app/src/main/java/org/sopt/official/domain/entity/attendance/Attendance.kt @@ -0,0 +1,111 @@ +package org.sopt.official.domain.entity.attendance + +import java.time.LocalDateTime + +data class Attendance( + val sessionId: Int, + val user: User, + val attendanceDayType: AttendanceDayType, +) { + data class User( + val name: String, + val generation: Int, + val part: Part, + val attendanceScore: Number, + val attendanceCount: AttendanceCount, + val attendanceHistory: List + ) { + enum class Part(val partName: String) { + PLAN("기획"), + DESIGN("디자인"), + ANDROID("안드로이드"), + IOS("iOS"), + WEB("웹"), + SERVER("서버"), + UNKNOWN("") + } + + data class AttendanceCount( + /** 출석 전체 횟수 */ + val attendanceCount: Int, + /** 지각 전체 횟수 */ + val lateCount: Int, + /** 결석 전체 횟수 */ + val absenceCount: Int, + ) { + /** 전체 횟수 */ + val totalCount: Int + get() = attendanceCount + lateCount + absenceCount + } + + data class AttendanceLog( + val sessionName: String, + val date: String, + val attendanceState: AttendanceState + ) { + enum class AttendanceState { + /** 참여(출석 체크 X)*/ + PARTICIPATE, + + /** 출석 */ + ATTENDANCE, + + /** 지각 */ + TARDY, + + /** 결석 */ + ABSENT + } + } + + companion object { + const val UNKNOWN_NAME = "회원" + const val UNKNOWN_GENERATION = -1 + const val UNKNOWN_PART = "UNKNOWN" + } + } + + sealed interface AttendanceDayType { + + /** 일정이 없는 날 */ + data object NoSession : AttendanceDayType + + /** 일정이 있고, 출석 체크가 있는 날 */ + data class HasAttendance( + val session: Session, + val firstRoundAttendance: RoundAttendance, + val secondRoundAttendance: RoundAttendance + ) : AttendanceDayType { + /** n차 출석에 관한 정보 */ + data class RoundAttendance( + val state: RoundAttendanceState, + val attendedAt: LocalDateTime? + ) { + /** n차 출석 상태 */ + enum class RoundAttendanceState { + ABSENT, ATTENDANCE, NOT_YET, + } + } + } + + /** 일정이 있고, 출석 체크가 없는 날 */ + data class NoAttendance(val session: Session) : AttendanceDayType + } + + /** 솝트의 세션에 관한 정보 + * @property name 세션 이름 (OT, 1차 세미나, 솝커톤 등) + * @property location 세션 장소, 정해진 장소가 없을 경우(온라인) null + * @property startAt 세션 시작 시각 + * @property endAt 세션 종료 시각 + * */ + data class Session( + val name: String, + val location: String?, + val startAt: LocalDateTime, + val endAt: LocalDateTime, + ) + + companion object { + const val UNKNOWN_SESSION_ID = -1 + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/domain/entity/attendance/ConfirmAttendanceCodeResult.kt b/app/src/main/java/org/sopt/official/domain/entity/attendance/ConfirmAttendanceCodeResult.kt new file mode 100644 index 000000000..5d0e690c0 --- /dev/null +++ b/app/src/main/java/org/sopt/official/domain/entity/attendance/ConfirmAttendanceCodeResult.kt @@ -0,0 +1,6 @@ +package org.sopt.official.domain.entity.attendance + +sealed interface ConfirmAttendanceCodeResult { + data object Success : ConfirmAttendanceCodeResult + data class Failure(val errorMessage: String?) : ConfirmAttendanceCodeResult +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/domain/entity/attendance/FetchAttendanceCurrentRoundResult.kt b/app/src/main/java/org/sopt/official/domain/entity/attendance/FetchAttendanceCurrentRoundResult.kt new file mode 100644 index 000000000..994badeb2 --- /dev/null +++ b/app/src/main/java/org/sopt/official/domain/entity/attendance/FetchAttendanceCurrentRoundResult.kt @@ -0,0 +1,6 @@ +package org.sopt.official.domain.entity.attendance + +sealed interface FetchAttendanceCurrentRoundResult { + data class Success(val round: Int?) : FetchAttendanceCurrentRoundResult + data class Failure(val errorMessage: String?) : FetchAttendanceCurrentRoundResult +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/domain/repository/attendance/NewAttendanceRepository.kt b/app/src/main/java/org/sopt/official/domain/repository/attendance/NewAttendanceRepository.kt new file mode 100644 index 000000000..bbf81add1 --- /dev/null +++ b/app/src/main/java/org/sopt/official/domain/repository/attendance/NewAttendanceRepository.kt @@ -0,0 +1,11 @@ +package org.sopt.official.domain.repository.attendance + +import org.sopt.official.domain.entity.attendance.Attendance +import org.sopt.official.domain.entity.attendance.ConfirmAttendanceCodeResult +import org.sopt.official.domain.entity.attendance.FetchAttendanceCurrentRoundResult + +interface NewAttendanceRepository { + suspend fun fetchAttendanceInfo(): Attendance + suspend fun fetchAttendanceCurrentRound(lectureId: Long): FetchAttendanceCurrentRoundResult + suspend fun confirmAttendanceCode(subLectureId: Long, code: String): ConfirmAttendanceCodeResult +} diff --git a/app/src/main/java/org/sopt/official/feature/attendance/AttendanceMapper.kt b/app/src/main/java/org/sopt/official/feature/attendance/AttendanceMapper.kt new file mode 100644 index 000000000..45b13a4df --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/AttendanceMapper.kt @@ -0,0 +1,67 @@ +package org.sopt.official.feature.attendance + +import kotlinx.collections.immutable.ImmutableMap +import kotlinx.collections.immutable.persistentMapOf +import org.sopt.official.domain.entity.attendance.Attendance +import org.sopt.official.domain.entity.attendance.Attendance.AttendanceDayType.HasAttendance.RoundAttendance.RoundAttendanceState +import org.sopt.official.feature.attendance.model.AttendanceDayType +import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceResultType +import org.sopt.official.feature.attendance.model.MidtermAttendance +import org.sopt.official.feature.attendance.model.MidtermAttendance.NotYet.AttendanceSession + +fun Attendance.AttendanceDayType.toUiAttendanceDayType(): AttendanceDayType { + return when (this) { + is Attendance.AttendanceDayType.HasAttendance -> { + AttendanceDayType.AttendanceDay.of( + session, + firstRoundAttendance, + secondRoundAttendance + ) + } + + is Attendance.AttendanceDayType.NoAttendance -> { + AttendanceDayType.Event.of(session) + } + + is Attendance.AttendanceDayType.NoSession -> { + AttendanceDayType.None + } + } +} + +fun Attendance.User.AttendanceCount.toTotalAttendanceResult(): ImmutableMap { + return persistentMapOf( + AttendanceResultType.ALL to totalCount, + AttendanceResultType.PRESENT to attendanceCount, + AttendanceResultType.LATE to lateCount, + AttendanceResultType.ABSENT to absenceCount, + ) +} + +fun Attendance.AttendanceDayType.HasAttendance.RoundAttendance.toUiFirstRoundAttendance(): MidtermAttendance { + return when (state) { + RoundAttendanceState.ATTENDANCE -> MidtermAttendance.Present( + attendanceAt = attendedAt.toString() + ) + + RoundAttendanceState.NOT_YET -> MidtermAttendance.NotYet( + attendanceSession = AttendanceSession.FIRST + ) + + RoundAttendanceState.ABSENT -> MidtermAttendance.Absent + } +} + +fun Attendance.AttendanceDayType.HasAttendance.RoundAttendance.toUiSecondRoundAttendance(): MidtermAttendance { + return when (state) { + RoundAttendanceState.ATTENDANCE -> MidtermAttendance.Present( + attendanceAt = attendedAt.toString() + ) + + RoundAttendanceState.NOT_YET -> MidtermAttendance.NotYet( + attendanceSession = AttendanceSession.SECOND + ) + + RoundAttendanceState.ABSENT -> MidtermAttendance.Absent + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceViewModel.kt b/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceViewModel.kt index 1a27461e7..4002fade6 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceViewModel.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceViewModel.kt @@ -1,37 +1,36 @@ package org.sopt.official.feature.attendance import androidx.lifecycle.ViewModel +import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -import org.sopt.official.domain.repository.attendance.AttendanceRepository +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import org.sopt.official.domain.entity.attendance.Attendance +import org.sopt.official.domain.repository.attendance.NewAttendanceRepository import org.sopt.official.feature.attendance.model.AttendanceUiState import javax.inject.Inject + @HiltViewModel class NewAttendanceViewModel @Inject constructor( - private val attendanceRepository: AttendanceRepository, + private val attendanceRepository: NewAttendanceRepository, ) : ViewModel() { init { - fetchData() + fetchAttendanceInfo() } - private val _uiState: MutableStateFlow = - MutableStateFlow(AttendanceUiState.Loading) + private val _uiState: MutableStateFlow = MutableStateFlow(AttendanceUiState.Loading) val uiState: StateFlow = _uiState - - fun fetchData() { - fetchSoptEvent() - fetchAttendanceHistory() - } - - private fun fetchSoptEvent() { - // TODO - } - - private fun fetchAttendanceHistory() { - // TODO + fun fetchAttendanceInfo() { + viewModelScope.launch { + val attendance: Attendance = attendanceRepository.fetchAttendanceInfo() + _uiState.update { + AttendanceUiState.Success.of(attendance) + } + } } } diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceCodeDialog.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceCodeDialog.kt new file mode 100644 index 000000000..8f557379f --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceCodeDialog.kt @@ -0,0 +1,155 @@ +package org.sopt.official.feature.attendance.compose + +import androidx.compose.foundation.background +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.painterResource +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider +import androidx.compose.ui.unit.dp +import androidx.compose.ui.window.Dialog +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import org.sopt.official.R +import org.sopt.official.designsystem.Black40 +import org.sopt.official.designsystem.Gray60 +import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.feature.attendance.compose.component.AttendanceCodeCardList +import org.sopt.official.feature.attendance.model.MidtermAttendance.NotYet.AttendanceSession + +@Composable +fun AttendanceCodeDialog( + codes: ImmutableList, + inputCodes: ImmutableList, + attendanceType: AttendanceSession, + onDismissRequest: () -> Unit, + modifier: Modifier = Modifier, +) { + Dialog(onDismissRequest = onDismissRequest) { + Column( + modifier + .background( + color = SoptTheme.colors.onSurface700, + shape = RoundedCornerShape(size = 10.dp) + ) + .padding(all = 16.dp), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Icon( + painter = painterResource(id = R.drawable.ic_close), + contentDescription = stringResource(id = R.string.close), + tint = SoptTheme.colors.onSurface10, + modifier = Modifier + .align(Alignment.End) + .clickable(onClick = onDismissRequest) + ) + Text( + text = stringResource(R.string.attendance_do, attendanceType.type), + style = SoptTheme.typography.heading18B, + color = SoptTheme.colors.onSurface10 + ) + Spacer(modifier = Modifier.height(10.dp)) + Text( + text = stringResource(R.string.attendance_code_description), + style = SoptTheme.typography.body13M, + color = SoptTheme.colors.onSurface300 + ) + Spacer(modifier = Modifier.height(24.dp)) + AttendanceCodeCardList( + codes = inputCodes, + onTextChange = {}, + onTextFieldFull = {}, + ) + if (codes != inputCodes) { + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = stringResource(R.string.attendance_code_does_not_match), + style = SoptTheme.typography.label12SB, + color = SoptTheme.colors.error + ) + } + Spacer(modifier = Modifier.height(32.dp)) + Button( + onClick = { /*TODO*/ }, + modifier = Modifier + .fillMaxWidth(), + shape = RoundedCornerShape(size = 6.dp), + colors = ButtonColors( + containerColor = SoptTheme.colors.onSurface10, + contentColor = SoptTheme.colors.onSurface950, + disabledContainerColor = Black40, + disabledContentColor = Gray60, + ), + enabled = codes == inputCodes + ) { + Text( + text = stringResource(R.string.attendance_dialog_button), + style = SoptTheme.typography.body13M, + ) + } + } + } +} + +@Preview +@Composable +private fun AttendanceCodeDialogPreview( + @PreviewParameter(AttendanceCodeDialogPreviewParameterProvider::class) parameter: AttendanceCodeDialogPreviewParameter, +) { + SoptTheme { + AttendanceCodeDialog( + codes = parameter.codes, + inputCodes = parameter.inputCodes, + attendanceType = parameter.attendanceType, + modifier = Modifier.fillMaxWidth(), + onDismissRequest = {} + ) + } +} + +data class AttendanceCodeDialogPreviewParameter( + val codes: ImmutableList, + val inputCodes: ImmutableList, + val attendanceType: AttendanceSession, +) + +class AttendanceCodeDialogPreviewParameterProvider : + PreviewParameterProvider { + override val values: Sequence = + sequenceOf( + AttendanceCodeDialogPreviewParameter( + codes = persistentListOf("1", "2", "3", "4", "5"), + inputCodes = persistentListOf("1", "2", "3", null, null), + AttendanceSession.FIRST, + ), + AttendanceCodeDialogPreviewParameter( + codes = persistentListOf("1", "2", "3", "4", "5"), + inputCodes = persistentListOf("1", "2", "3", "4", "5"), + AttendanceSession.FIRST, + ), + AttendanceCodeDialogPreviewParameter( + codes = persistentListOf("1", "2", "3", "4", "5"), + inputCodes = persistentListOf("1", "2", "3", null, null), + AttendanceSession.SECOND, + ), + AttendanceCodeDialogPreviewParameter( + codes = persistentListOf("1", "2", "3", "4", "5"), + inputCodes = persistentListOf("1", "2", "3", "4", "5"), + AttendanceSession.SECOND, + ), + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceRoute.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceRoute.kt index 30583e37e..057188948 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceRoute.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceRoute.kt @@ -1,9 +1,9 @@ package org.sopt.official.feature.attendance.compose +import androidx.compose.foundation.background import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.padding -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Scaffold import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue @@ -11,12 +11,12 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel +import org.sopt.official.designsystem.SoptTheme import org.sopt.official.feature.attendance.NewAttendanceViewModel import org.sopt.official.feature.attendance.compose.component.AttendanceTopAppBar import org.sopt.official.feature.attendance.model.AttendanceAction import org.sopt.official.feature.attendance.model.AttendanceUiState -@OptIn(ExperimentalMaterial3Api::class) @Composable fun AttendanceRoute(onClickBackIcon: () -> Unit) { val viewModel: NewAttendanceViewModel = viewModel() @@ -27,13 +27,14 @@ fun AttendanceRoute(onClickBackIcon: () -> Unit) { topBar = { AttendanceTopAppBar( onClickBackIcon = onClickBackIcon, - onClickRefreshIcon = viewModel::fetchData, + onClickRefreshIcon = action.onClickRefresh, ) } ) { innerPaddingValues -> Column( modifier = Modifier .fillMaxSize() + .background(color = SoptTheme.colors.background) .padding(innerPaddingValues) ) { when (state) { @@ -54,6 +55,6 @@ fun AttendanceRoute(onClickBackIcon: () -> Unit) { @Composable fun NewAttendanceViewModel.rememberAttendanceActions(): AttendanceAction = remember(this) { AttendanceAction( - onFakeClick = {} + onClickRefresh = ::fetchAttendanceInfo ) } diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceScreen.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceScreen.kt index e74e2b9bf..c1d20cb88 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceScreen.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceScreen.kt @@ -38,12 +38,11 @@ import org.sopt.official.feature.attendance.compose.component.TodayAttendanceCar import org.sopt.official.feature.attendance.compose.component.TodayNoAttendanceCard import org.sopt.official.feature.attendance.compose.component.TodayNoScheduleCard import org.sopt.official.feature.attendance.model.AttendanceAction +import org.sopt.official.feature.attendance.model.AttendanceDayType import org.sopt.official.feature.attendance.model.AttendanceUiState -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.FinalAttendance -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.MidtermAttendance import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceHistory import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceResultType +import org.sopt.official.feature.attendance.model.MidtermAttendance @Composable fun AttendanceScreen(state: AttendanceUiState.Success, action: AttendanceAction) { @@ -66,8 +65,8 @@ fun AttendanceScreen(state: AttendanceUiState.Success, action: AttendanceAction) eventDate = state.attendanceDayType.eventDate, eventLocation = state.attendanceDayType.eventLocation, eventName = state.attendanceDayType.eventName, - firstAttendance = state.attendanceDayType.firstAttendance, - secondAttendance = state.attendanceDayType.secondAttendance, + firstRoundAttendance = state.attendanceDayType.firstRoundAttendance, + secondRoundAttendance = state.attendanceDayType.secondRoundAttendance, finalAttendance = state.attendanceDayType.finalAttendance, ) } @@ -103,7 +102,7 @@ fun AttendanceScreen(state: AttendanceUiState.Success, action: AttendanceAction) modifier = Modifier .fillMaxWidth() .padding(bottom = 9.dp), - shape = RoundedCornerShape(size = 6.dp), + shape = RoundedCornerShape(size = 12.dp), colors = ButtonColors( containerColor = SoptTheme.colors.onSurface10, contentColor = SoptTheme.colors.onSurface950, @@ -142,7 +141,7 @@ private fun AttendanceScreenPreview(@PreviewParameter(AttendanceScreenPreviewPar state = AttendanceUiState.Success( attendanceDayType = parameter.attendanceDayType, userTitle = "32기 디자인파트 김솝트", - attendanceScore = 1, + attendanceScore = 2f, totalAttendanceResult = persistentMapOf( Pair(AttendanceResultType.ALL, 16), Pair(AttendanceResultType.PRESENT, 10), @@ -170,7 +169,7 @@ private fun AttendanceScreenPreview(@PreviewParameter(AttendanceScreenPreviewPar ), ), ), - action = AttendanceAction(onFakeClick = {}) + action = AttendanceAction(onClickRefresh = {}) ) } } @@ -182,7 +181,7 @@ data class AttendanceScreenPreviewParameter( ) -class AttendanceScreenPreviewParameterProvider() : +class AttendanceScreenPreviewParameterProvider : PreviewParameterProvider { override val values: Sequence = sequenceOf( @@ -191,9 +190,8 @@ class AttendanceScreenPreviewParameterProvider() : eventDate = "3월 23일 토요일 14:00 - 18:00", eventLocation = "건국대학교 꽥꽥오리관", eventName = "2차 세미나", - firstAttendance = MidtermAttendance.Present(attendanceAt = "14:00"), - secondAttendance = MidtermAttendance.Absent, - finalAttendance = FinalAttendance.LATE, + firstRoundAttendance = MidtermAttendance.Present(attendanceAt = "14:00"), + secondRoundAttendance = MidtermAttendance.Absent, ) ), AttendanceScreenPreviewParameter( attendanceDayType = AttendanceDayType.Event( diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeDialog.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeDialog.kt index 00568b876..40c06029d 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeDialog.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeDialog.kt @@ -28,7 +28,7 @@ import org.sopt.official.R import org.sopt.official.designsystem.Black40 import org.sopt.official.designsystem.Gray60 import org.sopt.official.designsystem.SoptTheme -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.MidtermAttendance.NotYet.AttendanceSession +import org.sopt.official.feature.attendance.model.MidtermAttendance.NotYet.AttendanceSession @Composable fun AttendanceCodeDialog( diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryCard.kt index be41474d7..248623434 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryCard.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryCard.kt @@ -24,7 +24,7 @@ import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.Atte @Composable fun AttendanceHistoryCard( userTitle: String, - attendanceScore: Int, + attendanceScore: Float, totalAttendanceResult: Map, attendanceHistoryList: ImmutableList, scrollState: ScrollState, @@ -72,7 +72,7 @@ private fun AttendanceHistoryCardPreview() { SoptTheme { AttendanceHistoryCard( userTitle = "32기 디자인파트 김솝트", - attendanceScore = 1, + attendanceScore = 1f, totalAttendanceResult = mapOf( Pair(AttendanceResultType.ALL, 16), Pair(AttendanceResultType.PRESENT, 5), diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryUserInfoCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryUserInfoCard.kt index edb08a6d0..191aa044e 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryUserInfoCard.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryUserInfoCard.kt @@ -13,6 +13,8 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.painterResource import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.tooling.preview.PreviewParameter +import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import org.sopt.official.R import org.sopt.official.designsystem.Orange400 @@ -21,7 +23,7 @@ import org.sopt.official.designsystem.SoptTheme @Composable fun AttendanceHistoryUserInfoCard( userTitle: String, - attendanceScore: Int, + attendanceScore: Float, modifier: Modifier = Modifier ) { Column(modifier = modifier) { @@ -38,7 +40,7 @@ fun AttendanceHistoryUserInfoCard( style = SoptTheme.typography.body18M ) Text( - text = "${attendanceScore}점", + text = "${attendanceScore.prettyString}점", color = Orange400, style = SoptTheme.typography.title20SB ) @@ -60,12 +62,26 @@ fun AttendanceHistoryUserInfoCard( @Preview @Composable -fun AttendanceHistoryUserInfoCardPreview() { +fun AttendanceHistoryUserInfoCardPreview( + @PreviewParameter(AttendanceHistoryUserInfoCardPreviewParameter::class) previewParameter: Float +) { SoptTheme { AttendanceHistoryUserInfoCard( userTitle = "32기 디자인파트 김솝트", - attendanceScore = 1, + attendanceScore = previewParameter, modifier = Modifier.background(color = SoptTheme.colors.onSurface800) ) } -} \ No newline at end of file +} + +class AttendanceHistoryUserInfoCardPreviewParameter(override val values: Sequence = sequenceOf(-0.5f, 0f, 0.5f, 1f, 1.5f, 2f)) : + PreviewParameterProvider + +private val Float.prettyString: String + get() { + return if (this == this.toInt().toFloat()) { + this.toInt().toString() + } else { + this.toString() + } + } \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceProgressBar.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceProgressBar.kt index 4dd011136..738d49685 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceProgressBar.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceProgressBar.kt @@ -14,9 +14,9 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import org.sopt.official.designsystem.SoptTheme -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.FinalAttendance -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.MidtermAttendance -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.MidtermAttendance.NotYet.AttendanceSession +import org.sopt.official.feature.attendance.model.FinalAttendance +import org.sopt.official.feature.attendance.model.MidtermAttendance +import org.sopt.official.feature.attendance.model.MidtermAttendance.NotYet.AttendanceSession @Composable fun AttendanceProgressBar( diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/FinalAttendanceCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/FinalAttendanceCard.kt index 4a10415ff..30635b56c 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/FinalAttendanceCard.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/FinalAttendanceCard.kt @@ -14,7 +14,7 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import org.sopt.official.designsystem.SoptTheme -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.FinalAttendance +import org.sopt.official.feature.attendance.model.FinalAttendance @Composable fun FinalAttendanceCard( diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/MidtermAttendanceCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/MidtermAttendanceCard.kt index 3c75cfeb0..e0aa98641 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/MidtermAttendanceCard.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/MidtermAttendanceCard.kt @@ -13,8 +13,8 @@ import androidx.compose.ui.tooling.preview.PreviewParameter import androidx.compose.ui.tooling.preview.PreviewParameterProvider import androidx.compose.ui.unit.dp import org.sopt.official.designsystem.SoptTheme -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.MidtermAttendance -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.MidtermAttendance.NotYet.AttendanceSession +import org.sopt.official.feature.attendance.model.MidtermAttendance +import org.sopt.official.feature.attendance.model.MidtermAttendance.NotYet.AttendanceSession @Composable fun MidtermAttendanceCard( diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayAttendanceCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayAttendanceCard.kt index 89f8c649b..33b09c439 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayAttendanceCard.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayAttendanceCard.kt @@ -20,16 +20,16 @@ import androidx.compose.ui.tooling.preview.Preview import androidx.compose.ui.unit.dp import org.sopt.official.R import org.sopt.official.designsystem.SoptTheme -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.FinalAttendance -import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceDayType.AttendanceDay.MidtermAttendance +import org.sopt.official.feature.attendance.model.FinalAttendance +import org.sopt.official.feature.attendance.model.MidtermAttendance @Composable fun TodayAttendanceCard( eventDate: String, eventLocation: String, eventName: String, - firstAttendance: MidtermAttendance, - secondAttendance: MidtermAttendance, + firstRoundAttendance: MidtermAttendance, + secondRoundAttendance: MidtermAttendance, finalAttendance: FinalAttendance, modifier: Modifier = Modifier, ) { @@ -88,8 +88,8 @@ fun TodayAttendanceCard( } Spacer(modifier = Modifier.height(12.dp)) AttendanceProgressBar( - firstAttendance = firstAttendance, - secondAttendance = secondAttendance, + firstAttendance = firstRoundAttendance, + secondAttendance = secondRoundAttendance, finalAttendance = finalAttendance, ) } @@ -106,8 +106,8 @@ private fun TodayAttendanceCardPreview() { eventDate = "3월 23일 토요일 14:00 - 18:00", eventLocation = "건국대학교 꽥꽥오리관", eventName = "2차 세미나", - firstAttendance = MidtermAttendance.Present(attendanceAt = "14:00"), - secondAttendance = MidtermAttendance.Absent, + firstRoundAttendance = MidtermAttendance.Present(attendanceAt = "14:00"), + secondRoundAttendance = MidtermAttendance.Absent, finalAttendance = FinalAttendance.LATE, ) } diff --git a/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceAction.kt b/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceAction.kt index 6fdba104b..339881408 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceAction.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceAction.kt @@ -1,5 +1,5 @@ package org.sopt.official.feature.attendance.model class AttendanceAction( - val onFakeClick: () -> Unit + val onClickRefresh: () -> Unit ) diff --git a/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceDayType.kt b/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceDayType.kt new file mode 100644 index 000000000..89fb1596e --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceDayType.kt @@ -0,0 +1,77 @@ +package org.sopt.official.feature.attendance.model + +import org.sopt.official.domain.entity.attendance.Attendance +import org.sopt.official.feature.attendance.toUiFirstRoundAttendance +import org.sopt.official.feature.attendance.toUiSecondRoundAttendance +import java.time.LocalDateTime +import java.time.format.DateTimeFormatter +import java.time.format.TextStyle +import java.util.Locale + +sealed interface AttendanceDayType { + /** 출석이 진행되는 날 **/ + data class AttendanceDay( + val eventDate: String, + val eventLocation: String, + val eventName: String, + val firstRoundAttendance: MidtermAttendance, + val secondRoundAttendance: MidtermAttendance, + ) : AttendanceDayType { + val finalAttendance: FinalAttendance = + FinalAttendance.calculateFinalAttendance(firstRoundAttendance, secondRoundAttendance) + + companion object { + fun of( + session: Attendance.Session, + firstRoundAttendance: Attendance.AttendanceDayType.HasAttendance.RoundAttendance, + secondRoundAttendance: Attendance.AttendanceDayType.HasAttendance.RoundAttendance + ): AttendanceDay { + return AttendanceDay( + eventDate = formatSessionTime(session.startAt, session.endAt), + eventLocation = session.location ?: "장소 정보를 불러올 수 없습니다.", + eventName = session.name, + firstRoundAttendance = firstRoundAttendance.toUiFirstRoundAttendance(), + secondRoundAttendance = secondRoundAttendance.toUiSecondRoundAttendance(), + ) + } + } + } + + /** 출석할 필요가 없는 날 **/ + data class Event( + val eventDate: String, + val eventLocation: String, + val eventName: String, + ) : AttendanceDayType { + companion object { + fun of(session: Attendance.Session): Event { + return Event( + eventDate = formatSessionTime(session.startAt, session.endAt), + eventLocation = session.location ?: "장소 정보를 불러올 수 없습니다.", + eventName = session.name + ) + } + } + } + + /** 아무 일정이 없는 날 **/ + data object None : AttendanceDayType +} + +private fun formatSessionTime(startAt: LocalDateTime, endAt: LocalDateTime): String { + val dateFormatter = DateTimeFormatter.ofPattern("M월 d일") + val timeFormatter = DateTimeFormatter.ofPattern("HH:mm") + + return "${startAt.format(dateFormatter)} ${ + startAt.dayOfWeek.getDisplayName( + TextStyle.FULL, Locale.KOREAN + ) + } ${ + startAt.format( + timeFormatter + ) + } - " + if (startAt.toLocalDate() == endAt.toLocalDate()) endAt.format(timeFormatter) + else "${endAt.format(dateFormatter)} ${ + endAt.dayOfWeek.getDisplayName(TextStyle.FULL, Locale.KOREAN) + } ${endAt.format(timeFormatter)}" +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceUiState.kt b/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceUiState.kt index 6527c2c53..d2205367f 100644 --- a/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceUiState.kt +++ b/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceUiState.kt @@ -1,96 +1,21 @@ package org.sopt.official.feature.attendance.model -import androidx.annotation.DrawableRes import kotlinx.collections.immutable.ImmutableList import kotlinx.collections.immutable.ImmutableMap -import org.sopt.official.R +import kotlinx.collections.immutable.toPersistentList +import org.sopt.official.domain.entity.attendance.Attendance +import org.sopt.official.feature.attendance.toTotalAttendanceResult +import org.sopt.official.feature.attendance.toUiAttendanceDayType sealed interface AttendanceUiState { data object Loading : AttendanceUiState data class Success( val attendanceDayType: AttendanceDayType, val userTitle: String, - val attendanceScore: Int, + val attendanceScore: Float, val totalAttendanceResult: ImmutableMap, val attendanceHistoryList: ImmutableList, ) : AttendanceUiState { - sealed interface AttendanceDayType { - /** 출석이 진행되는 날 **/ - data class AttendanceDay( - val eventDate: String, - val eventLocation: String, - val eventName: String, - val firstAttendance: MidtermAttendance, - val secondAttendance: MidtermAttendance, - val finalAttendance: FinalAttendance, - ) : AttendanceDayType { - sealed class MidtermAttendance( - @DrawableRes val imageResId: Int, - val isFinished: Boolean, - val description: String, - ) { - data class NotYet(val attendanceSession: AttendanceSession) : MidtermAttendance( - imageResId = R.drawable.ic_attendance_state_nothing, - isFinished = false, - description = attendanceSession.type - ) { - enum class AttendanceSession(val type: String) { - FIRST("1차 출석"), - SECOND("2차 출석") - } - } - - data class Present(val attendanceAt: String) : MidtermAttendance( - imageResId = R.drawable.ic_attendance_state_yes, - isFinished = true, - description = attendanceAt - ) - - data object Absent : MidtermAttendance( - imageResId = R.drawable.ic_attendance_state_absence_white, - isFinished = true, - description = "-" - ) - } - - enum class FinalAttendance( - @DrawableRes val imageResId: Int, - val isFinished: Boolean, - val result: String, - ) { - NOT_YET( - imageResId = R.drawable.ic_attendance_state_nothing, - isFinished = false, - result = "출석 전" - ), - PRESENT( - imageResId = R.drawable.ic_attendance_state_done, - isFinished = true, - result = "출석완료!" - ), - LATE( - imageResId = R.drawable.ic_attendance_state_late, - isFinished = true, - result = "지각" - ), - ABSENT( - imageResId = R.drawable.ic_attendance_state_absence_black, - isFinished = true, - result = "결석" - ) - } - } - - /** 출석할 필요가 없는 날 **/ - data class Event( - val eventDate: String, - val eventLocation: String, - val eventName: String, - ) : AttendanceDayType - - /** 아무 일정이 없는 날 **/ - data object None : AttendanceDayType - } enum class AttendanceResultType(val type: String) { ALL(type = "전체"), @@ -104,6 +29,29 @@ sealed interface AttendanceUiState { val eventName: String, val date: String, ) + + companion object { + fun of(attendance: Attendance): Success { + return Success( + attendanceDayType = attendance.attendanceDayType.toUiAttendanceDayType(), + userTitle = "${attendance.user.generation}기 ${attendance.user.part.partName}파트 ${attendance.user.name}", + attendanceScore = attendance.user.attendanceScore.toFloat(), + totalAttendanceResult = attendance.user.attendanceCount.toTotalAttendanceResult(), + attendanceHistoryList = attendance.user.attendanceHistory.map { attendanceLog: Attendance.User.AttendanceLog -> + AttendanceHistory( + status = when (attendanceLog.attendanceState) { + Attendance.User.AttendanceLog.AttendanceState.PARTICIPATE -> "참여" + Attendance.User.AttendanceLog.AttendanceState.ATTENDANCE -> "출석" + Attendance.User.AttendanceLog.AttendanceState.TARDY -> "지각" + Attendance.User.AttendanceLog.AttendanceState.ABSENT -> "결석" + }, + eventName = attendanceLog.sessionName, + date = attendanceLog.date + ) + }.toPersistentList() + ) + } + } } data class Failure(val error: Throwable?) : AttendanceUiState diff --git a/app/src/main/java/org/sopt/official/feature/attendance/model/FinalAttendance.kt b/app/src/main/java/org/sopt/official/feature/attendance/model/FinalAttendance.kt new file mode 100644 index 000000000..79dc8d29b --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/model/FinalAttendance.kt @@ -0,0 +1,45 @@ +package org.sopt.official.feature.attendance.model + +import androidx.annotation.DrawableRes +import org.sopt.official.R + +enum class FinalAttendance( + @DrawableRes val imageResId: Int, + val isFinished: Boolean, + val result: String, +) { + NOT_YET( + imageResId = R.drawable.ic_attendance_state_nothing, + isFinished = false, + result = "출석 전" + ), + PRESENT( + imageResId = R.drawable.ic_attendance_state_done, + isFinished = true, + result = "출석완료!" + ), + LATE( + imageResId = R.drawable.ic_attendance_state_late, + isFinished = true, + result = "지각" + ), + ABSENT( + imageResId = R.drawable.ic_attendance_state_absence_black, + isFinished = true, + result = "결석" + ); + + companion object { + fun calculateFinalAttendance( + firstAttendance: MidtermAttendance, + secondAttendance: MidtermAttendance, + ): FinalAttendance { + return when { + firstAttendance is MidtermAttendance.NotYet || secondAttendance is MidtermAttendance.NotYet -> NOT_YET + firstAttendance is MidtermAttendance.Present && secondAttendance is MidtermAttendance.Present -> PRESENT + firstAttendance is MidtermAttendance.Absent && secondAttendance is MidtermAttendance.Absent -> ABSENT + else -> LATE + } + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/feature/attendance/model/MidtermAttendance.kt b/app/src/main/java/org/sopt/official/feature/attendance/model/MidtermAttendance.kt new file mode 100644 index 000000000..97e42b948 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/model/MidtermAttendance.kt @@ -0,0 +1,34 @@ +package org.sopt.official.feature.attendance.model + +import androidx.annotation.DrawableRes +import org.sopt.official.R + + +sealed class MidtermAttendance private constructor( + @DrawableRes val imageResId: Int, + val isFinished: Boolean, + val description: String, +) { + data class NotYet(val attendanceSession: AttendanceSession) : MidtermAttendance( + imageResId = R.drawable.ic_attendance_state_nothing, + isFinished = false, + description = attendanceSession.type + ) { + enum class AttendanceSession(val type: String) { + FIRST("1차 출석"), + SECOND("2차 출석") + } + } + + data class Present(val attendanceAt: String) : MidtermAttendance( + imageResId = R.drawable.ic_attendance_state_yes, + isFinished = true, + description = attendanceAt + ) + + data object Absent : MidtermAttendance( + imageResId = R.drawable.ic_attendance_state_absence_white, + isFinished = true, + description = "-" + ) +} \ No newline at end of file diff --git a/app/src/test/java/org/sopt/official/FinalAttendanceTest.kt b/app/src/test/java/org/sopt/official/FinalAttendanceTest.kt new file mode 100644 index 000000000..0b53e57bc --- /dev/null +++ b/app/src/test/java/org/sopt/official/FinalAttendanceTest.kt @@ -0,0 +1,61 @@ +package org.sopt.official + +import org.junit.jupiter.api.Test +import org.sopt.official.feature.attendance.model.FinalAttendance +import org.sopt.official.feature.attendance.model.MidtermAttendance +import org.sopt.official.feature.attendance.model.MidtermAttendance.NotYet.AttendanceSession.FIRST +import org.sopt.official.feature.attendance.model.MidtermAttendance.NotYet.AttendanceSession.SECOND + +class FinalAttendanceTest { + private lateinit var firstRoundAttendance: MidtermAttendance + private lateinit var secondRoundAttendance: MidtermAttendance + + @Test + fun `1차 또는 2차 출석 여부가 아직 결정되지 않은 경우에는 최종 출석이 아직 결정되지 않은 상태로 한다`() { + firstRoundAttendance = MidtermAttendance.NotYet(FIRST) + secondRoundAttendance = MidtermAttendance.NotYet(SECOND) + assert(FinalAttendance.calculateFinalAttendance(firstRoundAttendance, secondRoundAttendance) == FinalAttendance.NOT_YET) + + firstRoundAttendance = MidtermAttendance.NotYet(FIRST) + secondRoundAttendance = MidtermAttendance.Present(attendanceAt = "16:00") + assert(FinalAttendance.calculateFinalAttendance(firstRoundAttendance, secondRoundAttendance) == FinalAttendance.NOT_YET) + + firstRoundAttendance = MidtermAttendance.NotYet(FIRST) + secondRoundAttendance = MidtermAttendance.Absent + assert(FinalAttendance.calculateFinalAttendance(firstRoundAttendance, secondRoundAttendance) == FinalAttendance.NOT_YET) + + firstRoundAttendance = MidtermAttendance.Present(attendanceAt = "14:00") + secondRoundAttendance = MidtermAttendance.NotYet(SECOND) + assert(FinalAttendance.calculateFinalAttendance(firstRoundAttendance, secondRoundAttendance) == FinalAttendance.NOT_YET) + + firstRoundAttendance = MidtermAttendance.Absent + secondRoundAttendance = MidtermAttendance.NotYet(SECOND) + assert(FinalAttendance.calculateFinalAttendance(firstRoundAttendance, secondRoundAttendance) == FinalAttendance.NOT_YET) + } + + @Test + fun `1차, 2차 출석 여부가 모두 출석일 경우 출석으로 한다`() { + firstRoundAttendance = MidtermAttendance.Present(attendanceAt = "14:00") + secondRoundAttendance = MidtermAttendance.Present(attendanceAt = "16:00") + assert(FinalAttendance.calculateFinalAttendance(firstRoundAttendance, secondRoundAttendance) == FinalAttendance.PRESENT) + } + + @Test + fun `1차, 2차 출석 여부가 모두 결석일 경우 결석으로 한다`() { + firstRoundAttendance = MidtermAttendance.Absent + secondRoundAttendance = MidtermAttendance.Absent + assert(FinalAttendance.calculateFinalAttendance(firstRoundAttendance, secondRoundAttendance) == FinalAttendance.ABSENT) + } + + @Test + fun `1차, 2차 출석 중 한 번은 출석하고 한 번은 결석한 경우 지각으로 한다`() { + firstRoundAttendance = MidtermAttendance.Present(attendanceAt = "14:00") + secondRoundAttendance = MidtermAttendance.Absent + assert(FinalAttendance.calculateFinalAttendance(firstRoundAttendance, secondRoundAttendance) == FinalAttendance.LATE) + + firstRoundAttendance = MidtermAttendance.Absent + secondRoundAttendance = MidtermAttendance.Present(attendanceAt = "16:00") + assert(FinalAttendance.calculateFinalAttendance(firstRoundAttendance, secondRoundAttendance) == FinalAttendance.LATE) + } + +}