diff --git a/app/build.gradle.kts b/app/build.gradle.kts index 14776ca16..847e091b3 100644 --- a/app/build.gradle.kts +++ b/app/build.gradle.kts @@ -148,6 +148,7 @@ dependencies { implementation(libs.kotlin.coroutines.google.play) implementation(platform(libs.compose.bom)) implementation(libs.bundles.compose) + implementation(libs.compose.lifecycle) implementation(libs.startup) implementation(libs.swipe.refresh.layout) debugImplementation(libs.compose.ui.tooling) @@ -177,6 +178,7 @@ dependencies { implementation(libs.coil.core) implementation(libs.profileinstaller) implementation(libs.firebase.messaging.lifecycle.ktx) + implementation(libs.kotlin.collections.immutable) } secrets { diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index 5f00b1dca..1f4fb8f74 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -153,5 +153,8 @@ + - \ No newline at end of file + 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/NewAttendanceActivity.kt b/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceActivity.kt new file mode 100644 index 000000000..dddd4b839 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceActivity.kt @@ -0,0 +1,22 @@ +package org.sopt.official.feature.attendance + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import dagger.hilt.android.AndroidEntryPoint +import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.feature.attendance.compose.AttendanceRoute + +@AndroidEntryPoint +class NewAttendanceActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContent { + SoptTheme { + AttendanceRoute( + onClickBackIcon = { if (!isFinishing) finish() } + ) + } + } + } +} 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 new file mode 100644 index 000000000..4002fade6 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/NewAttendanceViewModel.kt @@ -0,0 +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 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: NewAttendanceRepository, +) : ViewModel() { + + init { + fetchAttendanceInfo() + } + + private val _uiState: MutableStateFlow = MutableStateFlow(AttendanceUiState.Loading) + val uiState: StateFlow = _uiState + + 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/AttendanceLoadingScreen.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceLoadingScreen.kt new file mode 100644 index 000000000..f9857220b --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceLoadingScreen.kt @@ -0,0 +1,20 @@ +package org.sopt.official.feature.attendance.compose + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier + +@Composable +fun AttendanceLoadingScreen() { + Column( + modifier = Modifier.fillMaxSize(), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.Center + ) { + Text("Loading") + } +} 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 new file mode 100644 index 000000000..057188948 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceRoute.kt @@ -0,0 +1,60 @@ +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.Scaffold +import androidx.compose.runtime.Composable +import androidx.compose.runtime.getValue +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 + +@Composable +fun AttendanceRoute(onClickBackIcon: () -> Unit) { + val viewModel: NewAttendanceViewModel = viewModel() + val state by viewModel.uiState.collectAsStateWithLifecycle() + val action = viewModel.rememberAttendanceActions() + + Scaffold( + topBar = { + AttendanceTopAppBar( + onClickBackIcon = onClickBackIcon, + onClickRefreshIcon = action.onClickRefresh, + ) + } + ) { innerPaddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(color = SoptTheme.colors.background) + .padding(innerPaddingValues) + ) { + when (state) { + AttendanceUiState.Loading -> { + AttendanceLoadingScreen() + } + + is AttendanceUiState.Failure -> {} + AttendanceUiState.NetworkError -> {} + is AttendanceUiState.Success -> { + AttendanceScreen(state = state as AttendanceUiState.Success, action = action) + } + } + } + } +} + +@Composable +fun NewAttendanceViewModel.rememberAttendanceActions(): AttendanceAction = remember(this) { + AttendanceAction( + 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 new file mode 100644 index 000000000..c1d20cb88 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/AttendanceScreen.kt @@ -0,0 +1,207 @@ +package org.sopt.official.feature.attendance.compose + + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.fillMaxHeight +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.ButtonColors +import androidx.compose.material3.Scaffold +import androidx.compose.material3.Text +import androidx.compose.material3.TextButton +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +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 kotlinx.collections.immutable.persistentListOf +import kotlinx.collections.immutable.persistentMapOf +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.AttendanceGradientBox +import org.sopt.official.feature.attendance.compose.component.AttendanceHistoryCard +import org.sopt.official.feature.attendance.compose.component.AttendanceTopAppBar +import org.sopt.official.feature.attendance.compose.component.TodayAttendanceCard +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.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) { + val scrollState = rememberScrollState() + Box( + modifier = Modifier + .fillMaxSize() + .padding(horizontal = 20.dp), + contentAlignment = Alignment.BottomCenter + ) { + Column( + modifier = Modifier.fillMaxHeight(), + horizontalAlignment = Alignment.CenterHorizontally + ) { + Spacer(Modifier.height(16.dp)) + when (state.attendanceDayType) { + is AttendanceDayType.AttendanceDay -> { + TodayAttendanceCard( + modifier = Modifier.fillMaxWidth(), + eventDate = state.attendanceDayType.eventDate, + eventLocation = state.attendanceDayType.eventLocation, + eventName = state.attendanceDayType.eventName, + firstRoundAttendance = state.attendanceDayType.firstRoundAttendance, + secondRoundAttendance = state.attendanceDayType.secondRoundAttendance, + finalAttendance = state.attendanceDayType.finalAttendance, + ) + } + + is AttendanceDayType.Event -> { + TodayNoAttendanceCard( + modifier = Modifier.fillMaxWidth(), + eventDate = state.attendanceDayType.eventDate, + eventLocation = state.attendanceDayType.eventLocation, + eventName = state.attendanceDayType.eventLocation, + ) + } + + AttendanceDayType.None -> { + TodayNoScheduleCard( + modifier = Modifier.fillMaxWidth() + ) + } + } + Spacer(Modifier.height(20.dp)) + AttendanceHistoryCard( + userTitle = state.userTitle, + attendanceScore = state.attendanceScore, + totalAttendanceResult = state.totalAttendanceResult, + attendanceHistoryList = state.attendanceHistoryList, + scrollState = scrollState, + ) + Spacer(Modifier.height(36.dp)) + } + AttendanceGradientBox() + TextButton( + onClick = { /*TODO*/ }, + modifier = Modifier + .fillMaxWidth() + .padding(bottom = 9.dp), + shape = RoundedCornerShape(size = 12.dp), + colors = ButtonColors( + containerColor = SoptTheme.colors.onSurface10, + contentColor = SoptTheme.colors.onSurface950, + disabledContainerColor = Black40, + disabledContentColor = Gray60, + ), + contentPadding = PaddingValues(vertical = 16.dp) + ) { + Text( + text = stringResource(R.string.attendance_dialog_button), + style = SoptTheme.typography.label18SB, + ) + } + } +} + +@Preview +@Composable +private fun AttendanceScreenPreview(@PreviewParameter(AttendanceScreenPreviewParameterProvider::class) parameter: AttendanceScreenPreviewParameter) { + SoptTheme { + Scaffold( + topBar = { + AttendanceTopAppBar( + onClickBackIcon = { }, + onClickRefreshIcon = { } + ) + } + ) { innerPaddingValues -> + Column( + modifier = Modifier + .fillMaxSize() + .background(color = SoptTheme.colors.background) + .padding(innerPaddingValues) + ) { + AttendanceScreen( + state = AttendanceUiState.Success( + attendanceDayType = parameter.attendanceDayType, + userTitle = "32기 디자인파트 김솝트", + attendanceScore = 2f, + totalAttendanceResult = persistentMapOf( + Pair(AttendanceResultType.ALL, 16), + Pair(AttendanceResultType.PRESENT, 10), + Pair(AttendanceResultType.LATE, 4), + Pair(AttendanceResultType.ABSENT, 2) + ), + attendanceHistoryList = persistentListOf( + AttendanceHistory( + status = "출석", eventName = "1차 세미나", date = "00월 00일" + ), + AttendanceHistory( + status = "출석", eventName = "2차 세미나", date = "00월 00일" + ), + AttendanceHistory( + status = "출석", eventName = "3차 세미나", date = "00월 00일" + ), + AttendanceHistory( + status = "출석", eventName = "4차 세미나", date = "00월 00일" + ), + AttendanceHistory( + status = "출석", eventName = "5차 세미나", date = "00월 00일" + ), + AttendanceHistory( + status = "출석", eventName = "6차 세미나", date = "00월 00일" + ), + ), + ), + action = AttendanceAction(onClickRefresh = {}) + ) + } + } + } +} + +data class AttendanceScreenPreviewParameter( + val attendanceDayType: AttendanceDayType, +) + + +class AttendanceScreenPreviewParameterProvider : + PreviewParameterProvider { + + override val values: Sequence = sequenceOf( + AttendanceScreenPreviewParameter( + attendanceDayType = AttendanceDayType.AttendanceDay( + eventDate = "3월 23일 토요일 14:00 - 18:00", + eventLocation = "건국대학교 꽥꽥오리관", + eventName = "2차 세미나", + firstRoundAttendance = MidtermAttendance.Present(attendanceAt = "14:00"), + secondRoundAttendance = MidtermAttendance.Absent, + ) + ), AttendanceScreenPreviewParameter( + attendanceDayType = AttendanceDayType.Event( + eventDate = "3월 23일 토요일 14:00 - 18:00", + eventLocation = "건국대학교 꽥꽥오리관", + eventName = "2차 세미나", + ) + ), + AttendanceScreenPreviewParameter( + attendanceDayType = AttendanceDayType.None + ) + ) +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCard.kt new file mode 100644 index 000000000..c7eb86b7b --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCard.kt @@ -0,0 +1,68 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.foundation.text.BasicTextField +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.designsystem.SoptTheme + +@Composable +fun AttendanceCodeCard( + text: String, + onTextChange: (String) -> Unit, + onTextFieldFull: () -> Unit, + modifier: Modifier = Modifier, + textMaxLength: Int = 1, +) { + BasicTextField( + value = text, + onValueChange = { newText: String -> + if (newText.length < textMaxLength) { + onTextChange(newText) + } else { + onTextFieldFull() + } + }, + modifier = modifier + .background( + color = if (text.isEmpty()) SoptTheme.colors.onSurface600 + else SoptTheme.colors.onSurface800, + shape = RoundedCornerShape(8.dp) + ) + .border( + width = 1.dp, + color = if (text.isEmpty()) SoptTheme.colors.onSurface500 + else SoptTheme.colors.primary, + shape = RoundedCornerShape(8.dp) + ) + .padding(horizontal = 17.dp, vertical = 18.dp) + .width(10.dp), + textStyle = SoptTheme.typography.heading16B.copy(color = SoptTheme.colors.primary) + ) +} + +@Preview +@Composable +private fun AttendanceCodeCardPreview( + @PreviewParameter(AttendanceCodeCardPreviewParameterProvider::class) text: String, +) { + SoptTheme { + AttendanceCodeCard( + text = text, + onTextChange = {}, + onTextFieldFull = {} + ) + } +} + +private class AttendanceCodeCardPreviewParameterProvider : PreviewParameterProvider { + override val values: Sequence = sequenceOf("", "8") +} diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCardList.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCardList.kt new file mode 100644 index 000000000..b7e442c7f --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeCardList.kt @@ -0,0 +1,42 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.width +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.designsystem.SoptTheme + +@Composable +fun AttendanceCodeCardList( + codes: List, + onTextChange: (newText: String) -> Unit, + onTextFieldFull: () -> Unit, + modifier: Modifier = Modifier, +) { + Row(modifier = modifier) { + repeat(codes.size) { index -> + AttendanceCodeCard( + text = codes[index] ?: "", + onTextChange = onTextChange, + onTextFieldFull = onTextFieldFull + ) + if (index < codes.size) { + Spacer(modifier = Modifier.width(width = 12.dp)) + } + } + } +} + +@Preview +@Composable +private fun AttendanceCodeCardListPreview() { + SoptTheme { + AttendanceCodeCardList( + codes = listOf("8", "8", "8", null, null), + onTextChange = {}, + onTextFieldFull = {}) + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..40c06029d --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCodeDialog.kt @@ -0,0 +1,154 @@ +package org.sopt.official.feature.attendance.compose.component + +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.model.MidtermAttendance.NotYet.AttendanceSession + +@Composable +fun AttendanceCodeDialog( + codes: ImmutableList, + inputCodes: ImmutableList, + attendanceSession: 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, attendanceSession.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, + attendanceSession = parameter.attendanceSession, + modifier = Modifier.fillMaxWidth(), + onDismissRequest = {} + ) + } +} + +data class AttendanceCodeDialogPreviewParameter( + val codes: ImmutableList, + val inputCodes: ImmutableList, + val attendanceSession: 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/component/AttendanceCountCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCountCard.kt new file mode 100644 index 000000000..2dc1b69fe --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceCountCard.kt @@ -0,0 +1,67 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +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.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.R +import org.sopt.official.designsystem.SoptTheme + +@Composable +fun AttendanceCountCard( + resultType: String, + count: Int, + modifier: Modifier = Modifier +) { + Column(modifier = modifier, horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = resultType, + color = SoptTheme.colors.onSurface300, + style = SoptTheme.typography.label12SB + ) + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = count.toFormattedNumber(), + color = SoptTheme.colors.onSurface50, + style = SoptTheme.typography.body14M + ) + } +} + +private fun Int.toFormattedNumber(): String = "%02d회".format(this) + +@Preview +@Composable +private fun AttendanceCountCardPreview() { + SoptTheme { + Row { + AttendanceCountCard( + resultType = stringResource(id = R.string.title_attendance_count_all), + count = 0 + ) + Spacer(Modifier.width(10.dp)) + AttendanceCountCard( + resultType = stringResource(id = R.string.title_attendance_count_normal), + count = 0 + ) + Spacer(Modifier.width(10.dp)) + AttendanceCountCard( + resultType = stringResource(id = R.string.title_attendance_count_late), + count = 0 + ) + Spacer(Modifier.width(10.dp)) + AttendanceCountCard( + resultType = stringResource(id = R.string.title_attendance_count_abnormal), + count = 0 + ) + } + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceGradientBox.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceGradientBox.kt new file mode 100644 index 000000000..b6b2a1d6a --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceGradientBox.kt @@ -0,0 +1,36 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Brush +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp + +@Composable +fun AttendanceGradientBox(modifier: Modifier = Modifier) { + Box( + modifier = modifier + .fillMaxWidth() + .height(148.dp) + .background( + brush = Brush.verticalGradient( + colors = listOf( + Color(0x000F1010), Color(0xFF0F1010) + ) + ) + ) + .padding(bottom = 41.dp) + ) +} + +@Preview +@Composable +private fun AttendanceGradientBoxPreview() { + AttendanceGradientBox() +} \ No newline at end of file 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 new file mode 100644 index 000000000..248623434 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryCard.kt @@ -0,0 +1,96 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +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.rememberScrollState +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceHistory +import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceResultType + +@Composable +fun AttendanceHistoryCard( + userTitle: String, + attendanceScore: Float, + totalAttendanceResult: Map, + attendanceHistoryList: ImmutableList, + scrollState: ScrollState, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background( + color = SoptTheme.colors.onSurface800, + shape = RoundedCornerShape(16.dp) + ) + .padding(all = 32.dp) + ) { + AttendanceHistoryUserInfoCard( + userTitle = userTitle, + attendanceScore = attendanceScore + ) + Spacer(modifier = Modifier.height(24.dp)) + Row( + horizontalArrangement = Arrangement.SpaceBetween, + modifier = Modifier + .fillMaxWidth() + .background(color = SoptTheme.colors.onSurface700, shape = RoundedCornerShape(8.dp)) + .padding(horizontal = 24.dp, vertical = 16.dp), + ) { + totalAttendanceResult.forEach { attendanceResult -> + AttendanceCountCard( + resultType = attendanceResult.key.type, + count = attendanceResult.value + ) + } + } + Spacer(modifier = Modifier.height(24.dp)) + AttendanceHistoryListCard( + attendanceHistoryList = attendanceHistoryList, + scrollState = scrollState, + modifier = Modifier.fillMaxWidth() + ) + } +} + +@Preview +@Composable +private fun AttendanceHistoryCardPreview() { + SoptTheme { + AttendanceHistoryCard( + userTitle = "32기 디자인파트 김솝트", + attendanceScore = 1f, + totalAttendanceResult = mapOf( + Pair(AttendanceResultType.ALL, 16), + Pair(AttendanceResultType.PRESENT, 5), + Pair(AttendanceResultType.LATE, 0), + Pair(AttendanceResultType.ABSENT, 11) + ), + attendanceHistoryList = persistentListOf( + AttendanceHistory( + status = "출석", eventName = "1차 세미나", date = "00월 00일" + ), + AttendanceHistory( + status = "출석", eventName = "1차 세미나", date = "00월 00일" + ), + AttendanceHistory( + status = "출석", eventName = "1차 세미나", date = "00월 00일" + ), + ), + scrollState = rememberScrollState() + ) + } +} \ No newline at end of file diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryListCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryListCard.kt new file mode 100644 index 000000000..126ce0bf5 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryListCard.kt @@ -0,0 +1,92 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.ScrollState +import androidx.compose.foundation.gestures.Orientation +import androidx.compose.foundation.gestures.scrollable +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.rememberScrollState +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.style.TextOverflow +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.persistentListOf +import org.sopt.official.designsystem.SoptTheme +import org.sopt.official.feature.attendance.model.AttendanceUiState.Success.AttendanceHistory + +@Composable +fun AttendanceHistoryListCard( + attendanceHistoryList: ImmutableList, + scrollState: ScrollState, + modifier: Modifier = Modifier, +) { + Column(modifier = modifier) { + Text( + text = "나의 출결 현황", + color = SoptTheme.colors.onSurface300, + style = SoptTheme.typography.body14M, + ) + Spacer(Modifier.height(25.dp)) + Column( + Modifier.scrollable( + state = scrollState, + orientation = Orientation.Vertical + ) + ) { + attendanceHistoryList.forEachIndexed { index, attendanceHistory -> + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = attendanceHistory.status, + style = SoptTheme.typography.body14R, + color = SoptTheme.colors.onSurface100, + ) + Spacer(Modifier.width(8.dp)) + Text( + text = attendanceHistory.eventName, + style = SoptTheme.typography.label16SB, + color = SoptTheme.colors.onSurface10, + overflow = TextOverflow.Ellipsis, + modifier = Modifier.weight(1f) + ) + Spacer(Modifier.width(8.dp)) + Text( + text = attendanceHistory.date, + style = SoptTheme.typography.body14R, + color = SoptTheme.colors.onSurface100, + ) + } + if (index < attendanceHistoryList.lastIndex) { + Spacer(modifier = Modifier.height(16.dp)) + } + } + } + } +} + +@Preview(showBackground = true) +@Composable +private fun AttendanceHistoryListCardPreview() { + SoptTheme { + AttendanceHistoryListCard( + attendanceHistoryList = persistentListOf( + AttendanceHistory( + status = "출석", eventName = "1차 세미나", date = "00월 00일" + ), + AttendanceHistory( + status = "출석", eventName = "1차 세미나", date = "00월 00일" + ), + AttendanceHistory( + status = "출석", eventName = "1차 세미나", date = "00월 00일" + ), + ), + scrollState = rememberScrollState() + ) + } +} diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistorySummaryCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistorySummaryCard.kt new file mode 100644 index 000000000..2966befc5 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistorySummaryCard.kt @@ -0,0 +1,40 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.designsystem.SoptTheme + +@Composable +fun AttendanceHistorySummaryCard(modifier: Modifier = Modifier) { + Row(modifier) { + Column(horizontalAlignment = Alignment.CenterHorizontally) { + Text( + text = "전체", + color = SoptTheme.colors.onSurface300, + style = SoptTheme.typography.label12SB + ) + Spacer(Modifier.height(8.dp)) + Text( + text = "asdf", + color = SoptTheme.colors.onSurface50, + style = SoptTheme.typography.body14M + ) + } + } +} + +@Preview +@Composable +fun AttendanceHistorySummaryCardPreview() { + SoptTheme { + AttendanceHistorySummaryCard() + } +} \ No newline at end of file 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 new file mode 100644 index 000000000..191aa044e --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceHistoryUserInfoCard.kt @@ -0,0 +1,87 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +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.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 +import org.sopt.official.designsystem.SoptTheme + +@Composable +fun AttendanceHistoryUserInfoCard( + userTitle: String, + attendanceScore: Float, + modifier: Modifier = Modifier +) { + Column(modifier = modifier) { + Text( + text = userTitle, + color = SoptTheme.colors.onSurface300, + style = SoptTheme.typography.body14M + ) + Spacer(modifier = Modifier.height(8.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Text( + text = "현재 출석점수는 ", + color = SoptTheme.colors.onSurface10, + style = SoptTheme.typography.body18M + ) + Text( + text = "${attendanceScore.prettyString}점", + color = Orange400, + style = SoptTheme.typography.title20SB + ) + Text( + text = " 입니다!", + color = SoptTheme.colors.onSurface10, + style = SoptTheme.typography.body18M + ) + Spacer(modifier = Modifier.weight(1f)) + Icon( + modifier = Modifier.padding(end = 2.dp), + painter = painterResource(id = R.drawable.ic_attendance_point_info), + contentDescription = null, + tint = SoptTheme.colors.onSurface10 + ) + } + } +} + +@Preview +@Composable +fun AttendanceHistoryUserInfoCardPreview( + @PreviewParameter(AttendanceHistoryUserInfoCardPreviewParameter::class) previewParameter: Float +) { + SoptTheme { + AttendanceHistoryUserInfoCard( + userTitle = "32기 디자인파트 김솝트", + attendanceScore = previewParameter, + modifier = Modifier.background(color = SoptTheme.colors.onSurface800) + ) + } +} + +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 new file mode 100644 index 000000000..738d49685 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceProgressBar.kt @@ -0,0 +1,116 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.LinearProgressIndicator +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +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.designsystem.SoptTheme +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( + firstAttendance: MidtermAttendance, + secondAttendance: MidtermAttendance, + finalAttendance: FinalAttendance, + modifier: Modifier = Modifier +) { + Box(modifier = modifier) { + LinearProgressIndicator( + progress = { + calculateAttendanceProgress( + firstAttendance = firstAttendance, + secondAttendance = secondAttendance + ) + }, + modifier = Modifier + .fillMaxWidth() + .height(1.dp) + .padding(top = 24.dp, start = 36.dp, end = 36.dp), + color = SoptTheme.colors.onSurface10, + trackColor = SoptTheme.colors.onSurface400, + ) + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceBetween + ) { + MidtermAttendanceCard( + midtermAttendance = firstAttendance, + ) + MidtermAttendanceCard( + midtermAttendance = secondAttendance, + ) + FinalAttendanceCard( + finalAttendance = finalAttendance, + ) + } + } +} + +fun calculateAttendanceProgress( + firstAttendance: MidtermAttendance, + secondAttendance: MidtermAttendance, +): Float { + if (!firstAttendance.isFinished) return 0f + if (!secondAttendance.isFinished) { + return 0.5f + } else { + return 1f + } +} + + +class AttendanceProgressBarPreviewParameter( + val firstAttendance: MidtermAttendance, + val secondAttendance: MidtermAttendance, + val finalAttendance: FinalAttendance, +) + +class AttendanceProgressBarPreviewParameterProvider( + override val values: Sequence = sequenceOf( + AttendanceProgressBarPreviewParameter( + firstAttendance = MidtermAttendance.NotYet(AttendanceSession.FIRST), + secondAttendance = MidtermAttendance.NotYet(AttendanceSession.SECOND), + finalAttendance = FinalAttendance.NOT_YET, + ), + AttendanceProgressBarPreviewParameter( + firstAttendance = MidtermAttendance.Present(attendanceAt = "14:00"), + secondAttendance = MidtermAttendance.Absent, + finalAttendance = FinalAttendance.LATE, + ), + AttendanceProgressBarPreviewParameter( + firstAttendance = MidtermAttendance.Present(attendanceAt = "14:00"), + secondAttendance = MidtermAttendance.Present(attendanceAt = "16:00"), + finalAttendance = FinalAttendance.PRESENT, + ), + AttendanceProgressBarPreviewParameter( + firstAttendance = MidtermAttendance.Absent, + secondAttendance = MidtermAttendance.Absent, + finalAttendance = FinalAttendance.ABSENT, + ), + ), +) : PreviewParameterProvider + +@Preview(showBackground = true) +@Composable +private fun AttendanceProgressBarPreview( + @PreviewParameter(AttendanceProgressBarPreviewParameterProvider::class) attendanceProgressBarPreviewParameter: AttendanceProgressBarPreviewParameter, +) { + SoptTheme { + AttendanceProgressBar( + firstAttendance = attendanceProgressBarPreviewParameter.firstAttendance, + secondAttendance = attendanceProgressBarPreviewParameter.secondAttendance, + finalAttendance = attendanceProgressBarPreviewParameter.finalAttendance + ) + } +} diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceTopAppBar.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceTopAppBar.kt new file mode 100644 index 000000000..b209c5a9a --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/AttendanceTopAppBar.kt @@ -0,0 +1,72 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.clickable +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.CenterAlignedTopAppBar +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.Icon +import androidx.compose.material3.Text +import androidx.compose.material3.TopAppBarColors +import androidx.compose.runtime.Composable +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.unit.dp +import org.sopt.official.R +import org.sopt.official.designsystem.SoptTheme + +@OptIn(ExperimentalMaterial3Api::class) +@Composable +fun AttendanceTopAppBar( + onClickBackIcon: () -> Unit, + onClickRefreshIcon: () -> Unit, + modifier: Modifier = Modifier +) { + CenterAlignedTopAppBar( + title = { + Text( + text = stringResource(R.string.attendance_app_bar_title), + color = SoptTheme.colors.onSurface10, + style = SoptTheme.typography.body16M + ) + }, + modifier = modifier, + navigationIcon = { + Icon( + painter = painterResource(id = R.drawable.btn_arrow_left), + contentDescription = stringResource(R.string.go_back), + modifier = Modifier + .padding(start = 20.dp) + .clickable(onClick = onClickBackIcon) + ) + }, + actions = { + Icon( + painter = painterResource(id = R.drawable.ic_refresh), + contentDescription = stringResource(R.string.refresh), + modifier = Modifier + .padding(end = 20.dp) + .clickable(onClick = onClickRefreshIcon) + ) + }, + colors = TopAppBarColors( + containerColor = SoptTheme.colors.background, + scrolledContainerColor = SoptTheme.colors.background, + navigationIconContentColor = SoptTheme.colors.onBackground, + titleContentColor = SoptTheme.colors.onBackground, + actionIconContentColor = SoptTheme.colors.onBackground, + ) + ) +} + +@Preview +@Composable +private fun AttendanceTopAppBarPreview() { + SoptTheme { + AttendanceTopAppBar( + onClickBackIcon = {}, + onClickRefreshIcon = {}, + ) + } +} 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 new file mode 100644 index 000000000..30635b56c --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/FinalAttendanceCard.kt @@ -0,0 +1,60 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +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.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.designsystem.SoptTheme +import org.sopt.official.feature.attendance.model.FinalAttendance + +@Composable +fun FinalAttendanceCard( + finalAttendance: FinalAttendance, + modifier: Modifier = Modifier +) { + Column( + modifier = modifier.padding(horizontal = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(finalAttendance.imageResId), + contentDescription = null, + ) + Text( + text = finalAttendance.result, + color = if (finalAttendance.isFinished) SoptTheme.colors.onSurface10 else SoptTheme.colors.onSurface500, + style = SoptTheme.typography.label14SB + ) + } +} + +class FinalAttendanceCardPreviewParameterProvider( + override val values: Sequence = sequenceOf( + FinalAttendance.NOT_YET, + FinalAttendance.PRESENT, + FinalAttendance.LATE, + FinalAttendance.ABSENT, + ) +) : PreviewParameterProvider + +@Preview +@Composable +private fun FinalAttendanceCardPreview( + @PreviewParameter(FinalAttendanceCardPreviewParameterProvider::class) finalAttendance: FinalAttendance +) { + SoptTheme { + FinalAttendanceCard( + finalAttendance = finalAttendance, + modifier = Modifier.background(color = SoptTheme.colors.onSurface800) + ) + } +} 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 new file mode 100644 index 000000000..e0aa98641 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/MidtermAttendanceCard.kt @@ -0,0 +1,58 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.Image +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +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.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.designsystem.SoptTheme +import org.sopt.official.feature.attendance.model.MidtermAttendance +import org.sopt.official.feature.attendance.model.MidtermAttendance.NotYet.AttendanceSession + +@Composable +fun MidtermAttendanceCard( + midtermAttendance: MidtermAttendance, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier.padding(horizontal = 12.dp), + horizontalAlignment = Alignment.CenterHorizontally, + ) { + Image( + painter = painterResource(midtermAttendance.imageResId), + contentDescription = null, + ) + Text( + text = midtermAttendance.description, + color = if (midtermAttendance.isFinished) SoptTheme.colors.onSurface10 else SoptTheme.colors.onSurface500, + style = SoptTheme.typography.label14SB + ) + } +} + +class MidtermAttendanceCardPreviewParameterProvider( + override val values: Sequence = sequenceOf( + MidtermAttendance.NotYet(attendanceSession = AttendanceSession.FIRST), + MidtermAttendance.Present(attendanceAt = "14:00"), + MidtermAttendance.Absent, + ), +) : PreviewParameterProvider + +@Preview(showBackground = true) +@Composable +private fun MidtermAttendanceCardPreview( + @PreviewParameter(MidtermAttendanceCardPreviewParameterProvider::class) midtermAttendance: MidtermAttendance, +) { + SoptTheme { + MidtermAttendanceCard( + midtermAttendance = midtermAttendance + ) + } +} 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 new file mode 100644 index 000000000..33b09c439 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayAttendanceCard.kt @@ -0,0 +1,115 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.text.font.FontWeight +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.FinalAttendance +import org.sopt.official.feature.attendance.model.MidtermAttendance + +@Composable +fun TodayAttendanceCard( + eventDate: String, + eventLocation: String, + eventName: String, + firstRoundAttendance: MidtermAttendance, + secondRoundAttendance: MidtermAttendance, + finalAttendance: FinalAttendance, + modifier: Modifier = Modifier, +) { + Column( + modifier + .background( + color = SoptTheme.colors.onSurface800, + shape = RoundedCornerShape(16.dp) + ) + .padding(horizontal = 24.dp, vertical = 32.dp) + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(id = R.drawable.ic_attendance_event_date), + contentDescription = null, + tint = SoptTheme.colors.onSurface300, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = eventDate, + color = SoptTheme.colors.onSurface300, + style = SoptTheme.typography.body14M + ) + } + Spacer(modifier = Modifier.height(7.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(id = R.drawable.ic_attendance_event_location), + contentDescription = null, + tint = SoptTheme.colors.onSurface300, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = eventLocation, + color = SoptTheme.colors.onSurface300, + style = SoptTheme.typography.body14M + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Row { + Text( + text = stringResource(R.string.attendance_event_info_prefix), + color = SoptTheme.colors.onSurface10, + style = SoptTheme.typography.body18M + ) + Text( + text = eventName, + color = SoptTheme.colors.onSurface10, + style = SoptTheme.typography.body18M.copy(fontWeight = FontWeight.ExtraBold) + ) + Text( + text = stringResource(R.string.attendance_event_info_suffix), + color = SoptTheme.colors.onSurface10, + style = SoptTheme.typography.body18M + ) + } + Spacer(modifier = Modifier.height(12.dp)) + AttendanceProgressBar( + firstAttendance = firstRoundAttendance, + secondAttendance = secondRoundAttendance, + finalAttendance = finalAttendance, + ) + } +} + +@Preview +@Composable +private fun TodayAttendanceCardPreview() { + SoptTheme { + Column( + modifier = Modifier.background(color = SoptTheme.colors.background) + ) { + TodayAttendanceCard( + eventDate = "3월 23일 토요일 14:00 - 18:00", + eventLocation = "건국대학교 꽥꽥오리관", + eventName = "2차 세미나", + firstRoundAttendance = MidtermAttendance.Present(attendanceAt = "14:00"), + secondRoundAttendance = MidtermAttendance.Absent, + finalAttendance = FinalAttendance.LATE, + ) + } + } +} diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayNoAttendanceCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayNoAttendanceCard.kt new file mode 100644 index 000000000..f70e09d44 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayNoAttendanceCard.kt @@ -0,0 +1,105 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.Row +import androidx.compose.foundation.layout.Spacer +import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.width +import androidx.compose.foundation.shape.RoundedCornerShape +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.text.font.FontWeight +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.R +import org.sopt.official.designsystem.SoptTheme + +@Composable +fun TodayNoAttendanceCard( + eventDate: String, + eventLocation: String, + eventName: String, + modifier: Modifier = Modifier, +) { + Column( + modifier = modifier + .background( + color = SoptTheme.colors.onSurface800, + shape = RoundedCornerShape(16.dp) + ) + .padding(horizontal = 24.dp, vertical = 32.dp), + ) { + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(id = R.drawable.ic_attendance_event_date), + contentDescription = null, + tint = SoptTheme.colors.onSurface300, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = eventDate, + color = SoptTheme.colors.onSurface300, + style = SoptTheme.typography.body14M + ) + } + Spacer(modifier = Modifier.height(7.dp)) + Row(verticalAlignment = Alignment.CenterVertically) { + Icon( + painter = painterResource(id = R.drawable.ic_attendance_event_location), + contentDescription = null, + tint = SoptTheme.colors.onSurface300, + ) + Spacer(modifier = Modifier.width(4.dp)) + Text( + text = eventLocation, + color = SoptTheme.colors.onSurface300, + style = SoptTheme.typography.body14M + ) + } + Spacer(modifier = Modifier.height(16.dp)) + Row { + Text( + text = stringResource(R.string.attendance_event_info_prefix), + color = SoptTheme.colors.onSurface10, + style = SoptTheme.typography.body18M + ) + Text( + text = eventName, + color = SoptTheme.colors.onSurface10, + style = SoptTheme.typography.body18M.copy(fontWeight = FontWeight.ExtraBold) + ) + Text( + text = stringResource(R.string.attendance_event_info_suffix), + color = SoptTheme.colors.onSurface10, + style = SoptTheme.typography.body18M + ) + } + Spacer(modifier = Modifier.height(24.dp)) + Text( + text = stringResource(id = R.string.text_not_awarded), + color = SoptTheme.colors.onSurface100, + style = SoptTheme.typography.body14M, + ) + } +} + +@Preview +@Composable +private fun TodayNoAttendanceCardPreview() { + SoptTheme { + Column(modifier = Modifier.background(color = SoptTheme.colors.background)) { + TodayNoAttendanceCard( + eventDate = "5월 12일 일요일 14:00 - 18:00", + eventLocation = "배달의민족주문~", + eventName = "데모데이", + ) + } + } +} diff --git a/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayNoScheduleCard.kt b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayNoScheduleCard.kt new file mode 100644 index 000000000..38a219c0b --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/compose/component/TodayNoScheduleCard.kt @@ -0,0 +1,43 @@ +package org.sopt.official.feature.attendance.compose.component + +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.res.stringResource +import androidx.compose.ui.tooling.preview.Preview +import androidx.compose.ui.unit.dp +import org.sopt.official.R +import org.sopt.official.designsystem.SoptTheme + +@Composable +fun TodayNoScheduleCard(modifier: Modifier = Modifier) { + Column( + modifier = + modifier + .background( + color = SoptTheme.colors.onSurface800, + shape = RoundedCornerShape(16.dp) + ) + .padding(horizontal = 24.dp, vertical = 32.dp), + ) { + Text( + text = stringResource(id = R.string.text_no_schedule), + color = SoptTheme.colors.onSurface10, + style = SoptTheme.typography.title16SB, + ) + } +} + +@Preview +@Composable +private fun TodayNoScheduleCardPreview() { + SoptTheme { + Column(modifier = Modifier.background(color = SoptTheme.colors.background)) { + TodayNoScheduleCard() + } + } +} 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 new file mode 100644 index 000000000..339881408 --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceAction.kt @@ -0,0 +1,5 @@ +package org.sopt.official.feature.attendance.model + +class AttendanceAction( + 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 new file mode 100644 index 000000000..d2205367f --- /dev/null +++ b/app/src/main/java/org/sopt/official/feature/attendance/model/AttendanceUiState.kt @@ -0,0 +1,59 @@ +package org.sopt.official.feature.attendance.model + +import kotlinx.collections.immutable.ImmutableList +import kotlinx.collections.immutable.ImmutableMap +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: Float, + val totalAttendanceResult: ImmutableMap, + val attendanceHistoryList: ImmutableList, + ) : AttendanceUiState { + + enum class AttendanceResultType(val type: String) { + ALL(type = "전체"), + PRESENT(type = "출석"), + LATE(type = "지각"), + ABSENT(type = "결석"); + } + + data class AttendanceHistory( + val status: String, + 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 + data object NetworkError : 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/main/res/drawable/ic_attendance_state_absence_black.xml b/app/src/main/res/drawable/ic_attendance_state_absence_black.xml new file mode 100644 index 000000000..479a35f76 --- /dev/null +++ b/app/src/main/res/drawable/ic_attendance_state_absence_black.xml @@ -0,0 +1,51 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_attendance_state_absence_white.xml b/app/src/main/res/drawable/ic_attendance_state_absence_white.xml new file mode 100644 index 000000000..98be99fa3 --- /dev/null +++ b/app/src/main/res/drawable/ic_attendance_state_absence_white.xml @@ -0,0 +1,51 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_attendance_state_done.xml b/app/src/main/res/drawable/ic_attendance_state_done.xml new file mode 100644 index 000000000..f84c9c4be --- /dev/null +++ b/app/src/main/res/drawable/ic_attendance_state_done.xml @@ -0,0 +1,44 @@ + + + + + + diff --git a/app/src/main/res/drawable/ic_attendance_state_late.xml b/app/src/main/res/drawable/ic_attendance_state_late.xml new file mode 100644 index 000000000..c9e280985 --- /dev/null +++ b/app/src/main/res/drawable/ic_attendance_state_late.xml @@ -0,0 +1,51 @@ + + + + + + + diff --git a/app/src/main/res/drawable/ic_attendance_state_nothing.xml b/app/src/main/res/drawable/ic_attendance_state_nothing.xml new file mode 100644 index 000000000..9d73b8f05 --- /dev/null +++ b/app/src/main/res/drawable/ic_attendance_state_nothing.xml @@ -0,0 +1,34 @@ + + + + diff --git a/app/src/main/res/drawable/ic_attendance_state_yes.xml b/app/src/main/res/drawable/ic_attendance_state_yes.xml new file mode 100644 index 000000000..b0b278219 --- /dev/null +++ b/app/src/main/res/drawable/ic_attendance_state_yes.xml @@ -0,0 +1,44 @@ + + + + + + diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 30b3b3418..12e16e67e 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -1,5 +1,4 @@ - - 안녕하세요,\nSOPT의 열정이 되어주세요! @@ -83,12 +83,14 @@ 출석 + 전체 출석 지각 결석 참여 나의 출결 현황 출석점수를 체크하지 않습니다 + 오늘은 일정이 없는 날이에요 1차 출석 2차 출석 출석 전 @@ -98,6 +100,12 @@ 출석 코드 다섯 자리를 입력해주세요. 코드가 일치하지 않아요! 출석하기 + 뒤로 가기 + 새로고침 + 출석 조회하기 + %1$s하기 + 출석 코드 다섯 자리를 입력해주세요. + 코드가 일치하지 않아요! 알림 @@ -124,4 +132,6 @@ 접근할 수 없는 버튼이에요. 솝템프는 솝트 회원들에게만 제공되는 기능이에요. 확인 + "오늘은 " + " 날이에요" \ 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) + } + +} diff --git a/core/designsystem/src/main/java/org/sopt/official/designsystem/Color.kt b/core/designsystem/src/main/java/org/sopt/official/designsystem/Color.kt index 3e2bd0b50..bc991c545 100644 --- a/core/designsystem/src/main/java/org/sopt/official/designsystem/Color.kt +++ b/core/designsystem/src/main/java/org/sopt/official/designsystem/Color.kt @@ -40,6 +40,7 @@ val White = Color(0xFFFFFFFF) val Black = Color(0xFF000000) val Black80 = Color(0xFF1C1D1E) val Black60 = Color(0xFF2C2D2E) +val Black40 = Color(0xFF3C3D40) val Gray950 = Color(0xFF0F1012) val Gray900 = Color(0xFF17181C) val Gray800 = Color(0xFF202025) diff --git a/feature/soptamp/src/main/java/org/sopt/official/stamp/designsystem/style/Theme.kt b/feature/soptamp/src/main/java/org/sopt/official/stamp/designsystem/style/Theme.kt index c391a03fd..840c7c041 100644 --- a/feature/soptamp/src/main/java/org/sopt/official/stamp/designsystem/style/Theme.kt +++ b/feature/soptamp/src/main/java/org/sopt/official/stamp/designsystem/style/Theme.kt @@ -66,7 +66,7 @@ class SoptColors( onSurface20: Color, onSurface10: Color, onSurface5: Color, - isLight: Boolean + isLight: Boolean, ) { var white by mutableStateOf(white) private set @@ -217,7 +217,7 @@ fun soptLightColors( onSurface30: Color = Gray300, onSurface20: Color = Gray200, onSurface10: Color = Gray100, - onSurface5: Color = Gray50 + onSurface5: Color = Gray50, ) = SoptColors( white, black, @@ -278,7 +278,7 @@ fun soptDarkColors( onSurface30: Color = Gray300, onSurface20: Color = Gray200, onSurface10: Color = Gray100, - onSurface5: Color = Gray50 + onSurface5: Color = Gray50, ) = SoptColors( white, black, @@ -325,14 +325,18 @@ private val LocalSoptTypography = staticCompositionLocalOf { * Color에 접근하고 싶을때 SoptTheme.colors.primary 이런식으로 접근하면 됩니다. * Typo를 변경하고 싶다면 SoptTheme.typography.h1 이런식으로 접근하면 됩니다. * */ -object SoptTheme { +internal object SoptTheme { val colors: SoptColors @Composable get() = LocalSoptColors.current val typography: SoptTypography @Composable get() = LocalSoptTypography.current } @Composable -fun ProvideSoptColorsAndTypography(colors: SoptColors, typography: SoptTypography, content: @Composable () -> Unit) { +fun ProvideSoptColorsAndTypography( + colors: SoptColors, + typography: SoptTypography, + content: @Composable () -> Unit, +) { val provideColors = remember { colors.copy() } provideColors.update(colors) val provideTypography = remember { typography.copy() } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index 237c56d83..501295aa2 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -121,6 +121,7 @@ compose-lottie = { module = "com.airbnb.android:lottie-compose", version.ref = " compose-paging = { module = "androidx.paging:paging-compose", version.ref = "androidx-paging" } # For accesss of ComposeCompilerGradlePluginExtension compose-compiler-extension = { module = "org.jetbrains.kotlin:compose-compiler-gradle-plugin", version.ref = "kotlin" } +compose-lifecycle = { module = "androidx.lifecycle:lifecycle-runtime-compose" } coil-core = { module = "io.coil-kt:coil", version.ref = "coil" } coil-compose = { module = "io.coil-kt:coil-compose", version.ref = "coil" }