Skip to content

Commit

Permalink
feat: 특정 주차의 세션 출석률 통계 조회 기능 구현 (#37)
Browse files Browse the repository at this point in the history
* feat: 특정 주차의 세션 출석률 통계 조회 기능 구현

* refactor: 통계 집계 로직 usecase로 이동, Output 으로 넘기기
  • Loading branch information
ddingmin authored May 19, 2024
1 parent 96fdc1c commit f733972
Show file tree
Hide file tree
Showing 6 changed files with 174 additions and 32 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ enum class ErrorCode(
*/
INVALID_SESSION_PLACE("SE0001", "오프라인 세션의 장소가 기입이 필요합니다"),
SESSION_ALREADY_EXISTS("SE0002", "이미 해당 세션이 존재합니다"),
SESSION_NOT_FOUND("SE0003", "해당하는 세션을 찾을 수 없습니다"),

/**
* 출석 관련 오류
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@ open class SessionException(

class SessionPlaceNotFoundException : SessionException(ErrorCode.INVALID_SESSION_PLACE)
class SessionAlreadyExistsException : SessionException(ErrorCode.SESSION_ALREADY_EXISTS)
class SessionNotFoundException : SessionException(ErrorCode.SESSION_NOT_FOUND)
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.depromeet.makers.domain.usecase

import com.depromeet.makers.domain.exception.SessionNotFoundException
import com.depromeet.makers.domain.gateway.AttendanceGateway
import com.depromeet.makers.domain.gateway.SessionGateway
import com.depromeet.makers.domain.model.AttendanceStatus
import com.depromeet.makers.domain.model.MemberRole
import com.depromeet.makers.domain.model.Session
import com.depromeet.makers.domain.usecase.GetAttendancesByWeek.GetAttendancesByWeekOutput
import com.depromeet.makers.domain.usecase.GetAttendancesByWeek.GetAttendancesByWeekOutput.TeamAttendancesStats

class GetAttendancesByWeek(
private val attendanceGateway: AttendanceGateway,
private val sessionGateway: SessionGateway,
) : UseCase<GetAttendancesByWeek.GetAttendancesByWeekInput, GetAttendancesByWeekOutput> {
data class GetAttendancesByWeekInput(
val generation: Int,
val week: Int,
)

data class GetAttendancesByWeekOutput(
val session: Session,
val attendancePercentage: Double,
val totalAttendance: Int,
val totalMember: Int,
val teamAttendances: Map<Int, TeamAttendancesStats>,
) {
data class TeamAttendancesStats(
val teamNumber: Int,
var attendanceCount: Int,
var memberCount: Int,
)
}


override fun execute(input: GetAttendancesByWeekInput): GetAttendancesByWeekOutput {
val attendances = attendanceGateway.findAllByGenerationAndWeek(input.generation, input.week)
val session = sessionGateway.findByGenerationAndWeek(input.generation, input.week) ?: throw SessionNotFoundException()

var totalAttendanceCount = 0
var totalMemberCount = 0
val teams = mutableMapOf<Int, TeamAttendancesStats>()

attendances
.map { Pair(it.member.generations.find { generation -> generation.generationId == it.generation }, it) }
.filter { (memberGeneration, _) ->
// 현재 기수의 ROLE이 MEMBER인 경우만 집계
memberGeneration?.role == MemberRole.MEMBER
}
.filter { (_, attendance) ->
// 출석의 대상이 되는 인원들만 집계
attendance.attendanceStatus == AttendanceStatus.ATTENDANCE || attendance.attendanceStatus == AttendanceStatus.ATTENDANCE_ON_HOLD
}
.forEach { (memberGeneration, attendance) ->
val teamId = memberGeneration!!.groupId!!
val team = teams.getOrDefault(teamId, TeamAttendancesStats(teamId, 0, 0))
when (attendance.attendanceStatus) {
AttendanceStatus.ATTENDANCE -> {
team.attendanceCount++
team.memberCount++
totalAttendanceCount++
totalMemberCount++
}

AttendanceStatus.ATTENDANCE_ON_HOLD -> {
team.memberCount++
totalMemberCount++
}

else -> {
// do nothing
}
}
teams[teamId] = team
}
return GetAttendancesByWeekOutput(
session = session,
attendancePercentage = if (totalMemberCount == 0) 0.0 else (totalAttendanceCount.toDouble() / totalMemberCount.toDouble() * 100),
totalAttendance = totalAttendanceCount,
totalMember = totalMemberCount,
teamAttendances = teams,
)
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.depromeet.makers.presentation.restapi.controller

import com.depromeet.makers.domain.usecase.GetAttendancesByTeamAndWeek
import com.depromeet.makers.domain.usecase.GetAttendancesByWeek
import com.depromeet.makers.domain.usecase.GetMemberAttendances
import com.depromeet.makers.domain.usecase.UpdateAttendance
import com.depromeet.makers.presentation.restapi.dto.request.UpdateAttendanceRequest
import com.depromeet.makers.presentation.restapi.dto.response.AttendanceResponse
import com.depromeet.makers.presentation.restapi.dto.response.AttendanceStatsResponse
import com.depromeet.makers.presentation.restapi.dto.response.MyAttendanceResponse
import com.depromeet.makers.presentation.restapi.dto.response.UpdateAttendanceResponse
import io.swagger.v3.oas.annotations.Operation
Expand All @@ -22,6 +24,7 @@ class AttendanceController(
private val getMemberAttendances: GetMemberAttendances,
private val updateAttendance: UpdateAttendance,
private val getAttendancesByTeamAndWeek: GetAttendancesByTeamAndWeek,
private val getAttendancesByWeek: GetAttendancesByWeek,
) {

@Operation(summary = "나의 출석 현황 조회", description = "로그인한 사용자의 출석 현황을 조회합니다.")
Expand Down Expand Up @@ -72,4 +75,20 @@ class AttendanceController(
).map { AttendanceResponse.fromDomain(it) }
.sortedBy { it.memberName }
}

@Operation(summary = "팀 별 전체 출석 통계 조회")
@GetMapping("/stats")
fun getAttendanceStats(
@RequestParam(defaultValue = "15") generation: Int,
@RequestParam week: Int,
): AttendanceStatsResponse {
return AttendanceStatsResponse.fromDomain(
getAttendancesByWeek.execute(
GetAttendancesByWeek.GetAttendancesByWeekInput(
generation = generation,
week = week,
)
)
)
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
package com.depromeet.makers.presentation.restapi.dto.response

import com.depromeet.makers.domain.usecase.GetAttendancesByWeek.GetAttendancesByWeekOutput
import io.swagger.v3.oas.annotations.media.Schema
import java.time.LocalDateTime

@Schema(description = "출석 조회 결과 DTO")
data class AttendanceStatsResponse(

@Schema(description = "기수", example = "15")
val generation: Int,

@Schema(description = "주차", example = "1")
val week: Int,

@Schema(description = "세션 날짜", example = "2021-10-10T10:00:00")
val sessionDate: LocalDateTime,

@Schema(description = "전체 출석률", example = "50")
val attendancePercentage: Int,

@Schema(description = "출석 인원", example = "20")
val attendanceCount: Int,

@Schema(description = "오늘 세션에 출석 할 인원", example = "40")
val memberCount: Int,

@Schema(description = "출석한 팀 목록")
val teams: Map<Int, AttendanceStatsByTeamResponse>,
) {
data class AttendanceStatsByTeamResponse(
@Schema(description = "팀 번호", example = "1")
val teamNumber: Int,

@Schema(description = "팀 출석 인원", example = "4")
var attendanceCount: Int,

@Schema(description = "팀원 인원", example = "10")
var memberCount: Int,
) {
companion object {
fun new(teamNumber: Int, attendanceCount: Int, memberCount: Int) = AttendanceStatsByTeamResponse(
teamNumber = teamNumber,
attendanceCount = attendanceCount,
memberCount = memberCount,
)
}
}

companion object {
fun fromDomain(output: GetAttendancesByWeekOutput): AttendanceStatsResponse {
return AttendanceStatsResponse(
generation = output.session.generation,
week = output.session.week,
sessionDate = output.session.startTime,
attendancePercentage = output.attendancePercentage.toInt(),
attendanceCount = output.totalAttendance,
memberCount = output.totalMember,
teams = output.teamAttendances.mapValues { (teamId, teamAttendancesStats) ->
AttendanceStatsByTeamResponse.new(
teamNumber = teamId,
attendanceCount = teamAttendancesStats.attendanceCount,
memberCount = teamAttendancesStats.memberCount,
)
}
)
}
}
}

This file was deleted.

0 comments on commit f733972

Please sign in to comment.