Skip to content

Commit

Permalink
출석 정보 가져오기 구현 (#1015)
Browse files Browse the repository at this point in the history
* AttendanceCodeDialog 구현 (#852)

* Attendance screen compose setting (#676)

* feat: implement NewAttendanceActivity

* feat: implement NewAttendanceViewModel

* chore: add compose-lifecycle dependency

* feat: define AttendanceAction

* feat: implement screens

* feat: use SoptTheme in designsystem

* chore: data class -> class로 변경

* chore: 람다 프로퍼티 이름 명시

* chore: SoptTheme darkTheme 기본값 사용

* chore: 필요없는 함수 제거

* chore: 구현 안 된 함수에 TODO 삽입

* chore: 동작하지 않는 Preview 제거

* chore: AttendanceAction 내 뷰모델 참조 제거

* chore: code format 변경

* chore: make stamp design system internal

* chore: extract string resource

* feat: implement AttendanceCodeCard

* feat: implement AttendanceCodeCardList

* chore: change logic

* feat: implement AttendanceCodeDialog

* feat: implement attendance button

* chore: string resource 추출

* chore: change parameter List to ImmutableList

* chore: reformat codee

* feat: implement domain models

* feat: implement Repository

* chore: remove unnecessary code

* chore: change attendanceScore to Float

* chore: reflect type name changed

* chore: remove totalCount from parameters

* feat: add action onClickRefresh

* feat: implement bindDefaultAttendanceRepository

* chore: add background at route

* chore: change button radius

* chore: reformat code

* feat: implement Domain Model to UiModel

* chore: rename AttendanceInfo to Attendance

* chore: rename SessionInfo

* feat: format LocalDateTime

* chore: use DateTimeFormatter

* chore: use getOrNull

* chore: don't use it

* feat: implement AttendanceMapper

* chore: remove duplicate code

* chore: change to Extension

* chore: change parameter name

* chore: change code clear

* chore: make code clear

* chore: separate package

* chore: change function name

* feat: implement FinalAttendanceTest

* chore: remove duplicate tests
  • Loading branch information
giovannijunseokim authored Jan 10, 2025
1 parent d2b9c0d commit 816168f
Show file tree
Hide file tree
Showing 25 changed files with 835 additions and 137 deletions.
84 changes: 84 additions & 0 deletions app/src/main/java/org/sopt/official/data/AttendanceMapper.kt
Original file line number Diff line number Diff line change
@@ -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
}
}
}
Original file line number Diff line number Diff line change
@@ -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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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
Expand Down
Original file line number Diff line number Diff line change
@@ -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<AttendanceLog>
) {
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
}
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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
}
Original file line number Diff line number Diff line change
@@ -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<AttendanceResultType, Int> {
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
}
}
Loading

0 comments on commit 816168f

Please sign in to comment.