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" }