Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
@@ -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,
)
Original file line number Diff line number Diff line change
Expand Up @@ -21,12 +21,18 @@ class UserMyPageMapper(
user: User,
postCount: Long,
attendedSessionCount: Long,
penaltyCount: Int,
usingProfileMembers: List<ClubMember>,
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),
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,15 @@ class GetUserMyPageQueryService(
return getMyPageResponse(
userId = userId,
currentClubMemberId = currentMember.id,
penaltyCount = currentMember.penaltyCount,
currentProfile = currentMember.userProfile,
)
}

private fun getMyPageResponse(
userId: Long,
currentClubMemberId: Long,
penaltyCount: Int,
currentProfile: UserProfile?,
): UserMyPageResponse {
val user = userReader.getById(userId)
Expand All @@ -54,6 +56,7 @@ class GetUserMyPageQueryService(
user = user,
postCount = postCount,
attendedSessionCount = attendedSessionCount,
penaltyCount = penaltyCount,
usingProfileMembers = usingProfileMembers,
currentProfile = currentProfile,
)
Expand Down
Original file line number Diff line number Diff line change
@@ -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<UserMyPenaltyResponse> {
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
}
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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 = "현재 동아리 마이페이지 요약 조회")
Expand All @@ -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<PenaltyRuleResponse> =
CommonResponse.success(
UserResponseCode.USER_PENALTY_RULE_FIND_SUCCESS,
getPenaltyRuleQueryService.getRule(clubId, userId),
)
Comment on lines +65 to +75

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

userId도 파라미터로 넘겨서 사용자가 가입한 동아리인지를 확인하는(정책) 로직도 있어야 할 것 같아요!

@woneeeee woneeeee Aug 25, 2026

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

말씀해주신대로 userId를 서비스로 전달해서 ClubMemberPolicy를 통해서 활성 멤버 검증 로직 추가했슴니다!!


@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<SliceResponse<UserMyPenaltyResponse>> {
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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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, "패널티 규정을 성공적으로 조회했습니다."),
}
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -11,20 +13,24 @@ import io.mockk.mockk
class GetPenaltyRuleQueryServiceTest :
DescribeSpec({
val clubReader = mockk<ClubReader>()
val queryService = GetPenaltyRuleQueryService(clubReader)
val clubMemberPolicy = mockk<ClubMemberPolicy>()
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
}
Expand All @@ -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
}
Expand All @@ -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
}
Expand Down
Original file line number Diff line number Diff line change
@@ -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
Expand All @@ -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
Expand All @@ -24,15 +26,25 @@ class ClubMemberMyPageControllerTest :
val getUserPostQueryService = mockk<GetUserPostQueryService>()
val getUserAttendanceQueryService = mockk<GetUserAttendanceQueryService>()
val getUserMyPageQueryService = mockk<GetUserMyPageQueryService>()
val getUserPenaltyQueryService = mockk<GetUserPenaltyQueryService>()
val getPenaltyRuleQueryService = mockk<GetPenaltyRuleQueryService>()
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") {
Expand All @@ -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(
Expand Down