diff --git a/src/main/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyRuleQueryService.kt b/src/main/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyRuleQueryService.kt index eec2590e..ad7d2d7b 100644 --- a/src/main/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyRuleQueryService.kt +++ b/src/main/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyRuleQueryService.kt @@ -1,6 +1,7 @@ package com.weeth.domain.penalty.application.usecase.query import com.weeth.domain.club.domain.repository.ClubReader +import com.weeth.domain.club.domain.service.ClubMemberPolicy import com.weeth.domain.penalty.application.dto.response.PenaltyRuleResponse import org.springframework.stereotype.Service import org.springframework.transaction.annotation.Transactional @@ -9,8 +10,13 @@ import org.springframework.transaction.annotation.Transactional @Transactional(readOnly = true) class GetPenaltyRuleQueryService( private val clubReader: ClubReader, + private val clubMemberPolicy: ClubMemberPolicy, ) { - fun getRule(clubId: Long): PenaltyRuleResponse { + fun getRule( + clubId: Long, + userId: Long, + ): PenaltyRuleResponse { + clubMemberPolicy.getActiveMember(clubId, userId) val club = clubReader.getClubById(clubId) return PenaltyRuleResponse(content = club.penaltyRule) } diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserMyPageResponse.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserMyPageResponse.kt index f0caed01..a1417547 100644 --- a/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserMyPageResponse.kt +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserMyPageResponse.kt @@ -33,6 +33,8 @@ data class UserMyPageStatsResponse( val postCount: Long, @field:Schema(description = "출석한 세션 수", example = "8") val attendedSessionCount: Long, + @field:Schema(description = "패널티 횟수", example = "2") + val penaltyCount: Int, ) data class UserMyPageUsingProfileResponse( diff --git a/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserMyPenaltyResponse.kt b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserMyPenaltyResponse.kt new file mode 100644 index 00000000..61985478 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/dto/response/UserMyPenaltyResponse.kt @@ -0,0 +1,18 @@ +package com.weeth.domain.user.application.dto.response + +import com.weeth.domain.penalty.domain.enums.PenaltyType +import io.swagger.v3.oas.annotations.media.Schema +import java.time.LocalDateTime + +data class UserMyPenaltyResponse( + @field:Schema(description = "페널티 ID", example = "1") + val penaltyId: Long, + @field:Schema(description = "페널티 점수", example = "2") + val score: Int, + @field:Schema(description = "페널티 사유", example = "정기모임 무단 불참") + val penaltyDescription: String, + @field:Schema(description = "페널티 타입", example = "PENALTY") + val penaltyType: PenaltyType, + @field:Schema(description = "페널티 부여 일시", example = "2026-02-19T01:00:00") + val createdAt: LocalDateTime, +) diff --git a/src/main/kotlin/com/weeth/domain/user/application/mapper/UserMyPageMapper.kt b/src/main/kotlin/com/weeth/domain/user/application/mapper/UserMyPageMapper.kt index a31487c0..6828575a 100644 --- a/src/main/kotlin/com/weeth/domain/user/application/mapper/UserMyPageMapper.kt +++ b/src/main/kotlin/com/weeth/domain/user/application/mapper/UserMyPageMapper.kt @@ -21,12 +21,18 @@ class UserMyPageMapper( user: User, postCount: Long, attendedSessionCount: Long, + penaltyCount: Int, usingProfileMembers: List, currentProfile: UserProfile? = null, ): UserMyPageResponse = UserMyPageResponse( user = toInfoResponse(user), - stats = UserMyPageStatsResponse(postCount = postCount, attendedSessionCount = attendedSessionCount), + stats = + UserMyPageStatsResponse( + postCount = postCount, + attendedSessionCount = attendedSessionCount, + penaltyCount = penaltyCount, + ), usingProfiles = toUsingProfiles(usingProfileMembers), currentProfile = currentProfile?.let(::toCurrentProfile), ) diff --git a/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetUserMyPageQueryService.kt b/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetUserMyPageQueryService.kt index c5602216..a08f00c1 100644 --- a/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetUserMyPageQueryService.kt +++ b/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetUserMyPageQueryService.kt @@ -31,6 +31,7 @@ class GetUserMyPageQueryService( return getMyPageResponse( userId = userId, currentClubMemberId = currentMember.id, + penaltyCount = currentMember.penaltyCount, currentProfile = currentMember.userProfile, ) } @@ -38,6 +39,7 @@ class GetUserMyPageQueryService( private fun getMyPageResponse( userId: Long, currentClubMemberId: Long, + penaltyCount: Int, currentProfile: UserProfile?, ): UserMyPageResponse { val user = userReader.getById(userId) @@ -54,6 +56,7 @@ class GetUserMyPageQueryService( user = user, postCount = postCount, attendedSessionCount = attendedSessionCount, + penaltyCount = penaltyCount, usingProfileMembers = usingProfileMembers, currentProfile = currentProfile, ) diff --git a/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetUserPenaltyQueryService.kt b/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetUserPenaltyQueryService.kt new file mode 100644 index 00000000..19874063 --- /dev/null +++ b/src/main/kotlin/com/weeth/domain/user/application/usecase/query/GetUserPenaltyQueryService.kt @@ -0,0 +1,46 @@ +package com.weeth.domain.user.application.usecase.query + +import com.weeth.domain.club.domain.service.ClubMemberPolicy +import com.weeth.domain.penalty.domain.repository.PenaltyReader +import com.weeth.domain.user.application.dto.response.UserMyPenaltyResponse +import com.weeth.domain.user.application.exception.UserPageNotFoundException +import com.weeth.global.common.response.SliceResponse +import org.springframework.data.domain.PageRequest +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +@Service +@Transactional(readOnly = true) +class GetUserPenaltyQueryService( + private val penaltyReader: PenaltyReader, + private val clubMemberPolicy: ClubMemberPolicy, +) { + fun getMyPenalties( + userId: Long, + clubId: Long, + pageNumber: Int, + pageSize: Int, + ): SliceResponse { + if (pageNumber < 0 || pageSize !in 1..MAX_PAGE_SIZE) throw UserPageNotFoundException() + + val clubMember = clubMemberPolicy.getActiveMember(clubId, userId) + val pageable = PageRequest.of(pageNumber, pageSize) + val penalties = penaltyReader.findSliceByClubMemberId(clubMember.id, pageable) + + return SliceResponse.from( + penalties.map { penalty -> + UserMyPenaltyResponse( + penaltyId = penalty.id, + score = penalty.score, + penaltyDescription = penalty.penaltyDescription, + penaltyType = penalty.penaltyType, + createdAt = penalty.createdAt, + ) + }, + ) + } + + companion object { + private const val MAX_PAGE_SIZE = 50 + } +} diff --git a/src/main/kotlin/com/weeth/domain/user/presentation/ClubMemberMyPageController.kt b/src/main/kotlin/com/weeth/domain/user/presentation/ClubMemberMyPageController.kt index aaef54e0..14ee351b 100644 --- a/src/main/kotlin/com/weeth/domain/user/presentation/ClubMemberMyPageController.kt +++ b/src/main/kotlin/com/weeth/domain/user/presentation/ClubMemberMyPageController.kt @@ -1,12 +1,16 @@ package com.weeth.domain.user.presentation import com.weeth.domain.club.application.exception.ClubErrorCode +import com.weeth.domain.penalty.application.dto.response.PenaltyRuleResponse +import com.weeth.domain.penalty.application.usecase.query.GetPenaltyRuleQueryService import com.weeth.domain.user.application.dto.response.UserAttendedSessionResponse import com.weeth.domain.user.application.dto.response.UserMyPageResponse +import com.weeth.domain.user.application.dto.response.UserMyPenaltyResponse import com.weeth.domain.user.application.dto.response.UserMyPostResponse import com.weeth.domain.user.application.exception.UserErrorCode import com.weeth.domain.user.application.usecase.query.GetUserAttendanceQueryService import com.weeth.domain.user.application.usecase.query.GetUserMyPageQueryService +import com.weeth.domain.user.application.usecase.query.GetUserPenaltyQueryService import com.weeth.domain.user.application.usecase.query.GetUserPostQueryService import com.weeth.global.auth.annotation.CurrentUser import com.weeth.global.auth.jwt.application.exception.JwtErrorCode @@ -31,6 +35,8 @@ class ClubMemberMyPageController( private val getUserPostQueryService: GetUserPostQueryService, private val getUserAttendanceQueryService: GetUserAttendanceQueryService, private val getUserMyPageQueryService: GetUserMyPageQueryService, + private val getUserPenaltyQueryService: GetUserPenaltyQueryService, + private val getPenaltyRuleQueryService: GetPenaltyRuleQueryService, ) { @GetMapping @Operation(summary = "현재 동아리 마이페이지 요약 조회") @@ -56,6 +62,31 @@ class ClubMemberMyPageController( return CommonResponse.success(UserResponseCode.USER_MY_POSTS_FIND_SUCCESS, response) } + @GetMapping("/penalty-rule") + @Operation(summary = "현재 동아리 패널티 규정 조회") + fun getPenaltyRule( + @TsidParam + @TsidPathVariable clubId: Long, + @Parameter(hidden = true) @CurrentUser userId: Long, + ): CommonResponse = + CommonResponse.success( + UserResponseCode.USER_PENALTY_RULE_FIND_SUCCESS, + getPenaltyRuleQueryService.getRule(clubId, userId), + ) + + @GetMapping("/penalties") + @Operation(summary = "현재 동아리에서 나의 페널티 목록 조회") + fun getMyPenalties( + @TsidParam + @TsidPathVariable clubId: Long, + @Parameter(hidden = true) @CurrentUser userId: Long, + @RequestParam(defaultValue = "0") pageNumber: Int, + @RequestParam(defaultValue = "5") pageSize: Int, + ): CommonResponse> { + val response = getUserPenaltyQueryService.getMyPenalties(userId, clubId, pageNumber, pageSize) + return CommonResponse.success(UserResponseCode.USER_MY_PENALTIES_FIND_SUCCESS, response) + } + @GetMapping("/attended-sessions") @Operation(summary = "현재 동아리에서 출석한 세션 조회") fun getAttendedSessions( diff --git a/src/main/kotlin/com/weeth/domain/user/presentation/UserResponseCode.kt b/src/main/kotlin/com/weeth/domain/user/presentation/UserResponseCode.kt index c0f814ad..8d67edbe 100644 --- a/src/main/kotlin/com/weeth/domain/user/presentation/UserResponseCode.kt +++ b/src/main/kotlin/com/weeth/domain/user/presentation/UserResponseCode.kt @@ -26,4 +26,6 @@ enum class UserResponseCode( USER_PROFILE_IMAGE_DELETED_SUCCESS(10916, HttpStatus.OK, "프로필 사진이 성공적으로 삭제되었습니다."), USER_PROFILE_HEADER_IMAGE_DELETED_SUCCESS(10917, HttpStatus.OK, "프로필 헤더 사진이 성공적으로 삭제되었습니다."), USER_PROFILE_ASSIGNABLE_CLUBS_FIND_SUCCESS(10918, HttpStatus.OK, "프로필을 사용할 수 있는 동아리 목록을 성공적으로 조회했습니다."), + USER_MY_PENALTIES_FIND_SUCCESS(10919, HttpStatus.OK, "페널티 목록을 성공적으로 조회했습니다."), + USER_PENALTY_RULE_FIND_SUCCESS(10920, HttpStatus.OK, "패널티 규정을 성공적으로 조회했습니다."), } diff --git a/src/test/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyRuleQueryServiceTest.kt b/src/test/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyRuleQueryServiceTest.kt index e9fa9096..00e7d1a6 100644 --- a/src/test/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyRuleQueryServiceTest.kt +++ b/src/test/kotlin/com/weeth/domain/penalty/application/usecase/query/GetPenaltyRuleQueryServiceTest.kt @@ -1,6 +1,8 @@ package com.weeth.domain.penalty.application.usecase.query import com.weeth.domain.club.domain.repository.ClubReader +import com.weeth.domain.club.domain.service.ClubMemberPolicy +import com.weeth.domain.club.fixture.ClubMemberTestFixture import com.weeth.domain.club.fixture.ClubTestFixture import io.kotest.core.spec.style.DescribeSpec import io.kotest.matchers.shouldBe @@ -11,20 +13,24 @@ import io.mockk.mockk class GetPenaltyRuleQueryServiceTest : DescribeSpec({ val clubReader = mockk() - val queryService = GetPenaltyRuleQueryService(clubReader) + val clubMemberPolicy = mockk() + val queryService = GetPenaltyRuleQueryService(clubReader, clubMemberPolicy) + + val userId = 1L + val clubMember = ClubMemberTestFixture.createActiveMember() beforeTest { - clearMocks(clubReader) + clearMocks(clubReader, clubMemberPolicy) + every { clubMemberPolicy.getActiveMember(any(), userId) } returns clubMember } describe("getRule") { it("클럽의 페널티 규정을 조회한다") { - val ruleContent = "1. 정기모임 무단 불참: 5점\n2. 지각: 2점" val club = ClubTestFixture.createClub() // 직접 penaltyRule을 설정할 방법을 찾아야 함 every { clubReader.getClubById(club.id) } returns club - val response = queryService.getRule(club.id) + val response = queryService.getRule(club.id, userId) response.content shouldBe club.penaltyRule } @@ -33,7 +39,7 @@ class GetPenaltyRuleQueryServiceTest : val club = ClubTestFixture.createClub() every { clubReader.getClubById(club.id) } returns club - val response = queryService.getRule(club.id) + val response = queryService.getRule(club.id, userId) response.content shouldBe club.penaltyRule } @@ -52,7 +58,7 @@ class GetPenaltyRuleQueryServiceTest : val club = ClubTestFixture.createClub() every { clubReader.getClubById(club.id) } returns club - val response = queryService.getRule(club.id) + val response = queryService.getRule(club.id, userId) response.content shouldBe club.penaltyRule } diff --git a/src/test/kotlin/com/weeth/domain/user/presentation/ClubMemberMyPageControllerTest.kt b/src/test/kotlin/com/weeth/domain/user/presentation/ClubMemberMyPageControllerTest.kt index 0cfd8806..f39dc904 100644 --- a/src/test/kotlin/com/weeth/domain/user/presentation/ClubMemberMyPageControllerTest.kt +++ b/src/test/kotlin/com/weeth/domain/user/presentation/ClubMemberMyPageControllerTest.kt @@ -1,6 +1,7 @@ package com.weeth.domain.user.presentation import com.weeth.domain.attendance.domain.enums.AttendanceStatus +import com.weeth.domain.penalty.application.usecase.query.GetPenaltyRuleQueryService import com.weeth.domain.user.application.dto.response.UserAttendedSessionResponse import com.weeth.domain.user.application.dto.response.UserMyPageCurrentProfileResponse import com.weeth.domain.user.application.dto.response.UserMyPageInfoResponse @@ -9,6 +10,7 @@ import com.weeth.domain.user.application.dto.response.UserMyPageStatsResponse import com.weeth.domain.user.application.dto.response.UserMyPostResponse import com.weeth.domain.user.application.usecase.query.GetUserAttendanceQueryService import com.weeth.domain.user.application.usecase.query.GetUserMyPageQueryService +import com.weeth.domain.user.application.usecase.query.GetUserPenaltyQueryService import com.weeth.domain.user.application.usecase.query.GetUserPostQueryService import com.weeth.global.common.response.SliceResponse import io.kotest.core.spec.style.DescribeSpec @@ -24,15 +26,25 @@ class ClubMemberMyPageControllerTest : val getUserPostQueryService = mockk() val getUserAttendanceQueryService = mockk() val getUserMyPageQueryService = mockk() + val getUserPenaltyQueryService = mockk() + val getPenaltyRuleQueryService = mockk() val controller = ClubMemberMyPageController( getUserPostQueryService = getUserPostQueryService, getUserAttendanceQueryService = getUserAttendanceQueryService, getUserMyPageQueryService = getUserMyPageQueryService, + getUserPenaltyQueryService = getUserPenaltyQueryService, + getPenaltyRuleQueryService = getPenaltyRuleQueryService, ) beforeTest { - clearMocks(getUserPostQueryService, getUserAttendanceQueryService, getUserMyPageQueryService) + clearMocks( + getUserPostQueryService, + getUserAttendanceQueryService, + getUserMyPageQueryService, + getUserPenaltyQueryService, + getPenaltyRuleQueryService, + ) } describe("getSummary") { @@ -48,7 +60,7 @@ class ClubMemberMyPageControllerTest : department = "컴퓨터공학과", studentId = "20201234", ), - stats = UserMyPageStatsResponse(postCount = 12L, attendedSessionCount = 8L), + stats = UserMyPageStatsResponse(postCount = 12L, attendedSessionCount = 8L, penaltyCount = 0), usingProfiles = emptyList(), currentProfile = UserMyPageCurrentProfileResponse(