Skip to content

[WTH-468] 백엔드 패널티 관리 UI 변경에 따른 수정사항 반영 - #97

Merged
woneeeee merged 41 commits into
devfrom
fix/WTH-468-백엔드-패널티-관리-UI-변경에-따른-수정사항-반영
Aug 25, 2026

Hidden character warning

The head ref may contain hidden characters: "fix/WTH-468-\ubc31\uc5d4\ub4dc-\ud328\ub110\ud2f0-\uad00\ub9ac-UI-\ubcc0\uacbd\uc5d0-\ub530\ub978-\uc218\uc815\uc0ac\ud56d-\ubc18\uc601"
Merged

[WTH-468] 백엔드 패널티 관리 UI 변경에 따른 수정사항 반영#97
woneeeee merged 41 commits into
devfrom
fix/WTH-468-백엔드-패널티-관리-UI-변경에-따른-수정사항-반영

Conversation

@woneeeee

@woneeeee woneeeee commented Aug 24, 2026

Copy link
Copy Markdown
Member

📌 Summary

어떤 작업인지 한 줄 요약해 주세요.

페널티 관리 UI 개편에 맞춰 페널티 관련 API를 전반적으로 수정하고, 마이페이지 및 어드민 페이지에 페널티 관련 기능을 추가합니다.

📝 Changes

1. 페널티 부여 수정 POST /api/v4/admin/clubs/{clubId}/penalties

  • 단일 멤버 → 다수 멤버 동시 부여 (userIds: List<Long>)
  • 점수제 도입 (score: Int, 기본값 1) — 한 번에 여러 점수 부여 가능
  • 페널티 사유 필수 (penaltyDescription: @NotBlank)

2. 페널티 수정 추가 PUT /api/v4/admin/clubs/{clubId}/penalties/{penaltyId}

  • 페널티 사유 + 점수 수정 가능
  • 점수 변경 시 ClubMember.penaltyCount / warningCount 자동 delta 보정

3. 페널티 삭제 수정 DELETE /api/v4/admin/clubs/{clubId}/penalties/{penaltyId}

  • 기존 카운트 1 차감 → 삭제된 페널티의 score만큼 차감

4. 어드민 멤버 페널티 상세 조회 신규 GET /api/v4/admin/clubs/{clubId}/penalties/members/{clubMemberId}

  • 응답: 프로필 이미지, 이름, 활동 기수 목록, 활동 상태, 자기소개, 페널티 목록 (사유/점수/날짜)

5. 페널티 규정 저장 신규 PUT /api/v4/admin/clubs/{clubId}/penalties/rule

  • 어드민이 동아리별 페널티 규정 텍스트 저장 (최대 500자, null 허용)

6. 어드민 멤버 목록에 페널티 정보 추가 GET /api/v4/admin/clubs/{clubId}/members

  • 응답에 penaltyCount, lastPenaltyAt 추가

7. 마이페이지 stats에 페널티 갯수 추가 GET /api/v4/clubs/{clubId}/users/me/mypage

  • stats.penaltyCount 추가

8. 마이페이지 페널티 목록 조회 신규 GET /api/v4/clubs/{clubId}/users/me/mypage/penalties

  • 무한스크롤 (SliceResponse)
  • 응답: 페널티 ID, 점수, 사유, 타입, 생성일

9. 마이페이지 페널티 규정 조회 신규 GET /api/v4/clubs/{clubId}/users/me/mypage/penalty-rule

  • 해당 동아리의 페널티 규정 텍스트 반환

10. 한국어 표기 일괄 수정

  • "패널티" → "페널티" (응답 메시지, 주석 등)

11. 어드민 멤버 검색 조회 신규 GET /api/v4/admin/clubs/{clubId}/members/search

  • 멤버 이름으로 검색 (keyword 필수)
  • 전체 또는 특정 기수(cardinalNumber) 필터링
  • 페이지네이션 지원 (page, size)
  • 응답: 기존 멤버 목록 조회와 동일한 ClubMemberResponse 반환

페널티 / 경고 분리 현황

현재 PenaltyType enum으로 PENALTY / WARNING 두 타입이 구분되어 있으며, ClubMemberpenaltyCountwarningCount가 별도로 관리됩니다.

현재 상태: Club.warningEnabled의 기본값이 false이므로, 프론트에서 penaltyType: "WARNING"을 전송하면 WarningNotEnabledException이 발생합니다. 경고 타입은 현재 실제로 사용할 수 없습니다.

경고 기능 활성화를 위해 필요한 것:

  • 동아리별 경고 기능 ON/OFF API 구현 (PUT /api/v4/admin/clubs/{clubId}/warning-enabled 예정)
  • 해당 API로 warningEnabled = true 설정 후 프론트에서 penaltyType: "WARNING" 전송 시 warningCount 증가
  • 경고 누적 시 자동 페널티 전환 규칙이 필요하다면 별도 도메인 로직 추가 필요

테스트 항목

  • UpdatePenaltyUseCase (15개): 페널티 수정, 카운트 조정, 권한 검증
  • GetPenaltyQueryService (8개): 기수별/멤버별 조회, 권한 검증
  • SavePenaltyRuleUseCase (4개): 규정 저장/초기화
  • GetPenaltyRuleQueryService (3개): 규정 조회
  • Penalty Entity (8개): 상태 전환, 유효성 검증, 비즈니스 로직
  • GetClubMemberQueryService (4개): 이름 검색, 기수별 필터링, 결과 없음 처리, 정렬
  • ClubAdminController (4개): 응답 코드, 기수 검색, 검색 결과 없음, 페이지네이션

📸 Screenshots / Logs

필요시 스크린샷 or 로그를 첨부해주세요.

image image image

💡 Reviewer 참고사항

리뷰에 참고할 내용을 작성해주세요.

슬랙에도 적어뒀는데 페널티에서 멤버 검색을 할때 해당 기수 전체 멤버 검색 api가 필요할 것 같습니다! 페널티 도메인보다 어드민 멤버 목록 API에 검색 파라미터 추가하거나 따로 공통 검색 API를 분리하는게 나을 것 같은데 어떻게 생각하시나욤...?? 의견 주시면 해당 피알이나 다음 피알에서 함께 반영해보겠습니닷...!! -> 구현 완료

✅ Checklist

  • PR 제목 설정 완료 (WTH-123 인증 필터 설정)
  • 테스트 구현 완료
  • 리뷰어 등록 완료
  • 자체 코드 리뷰 완료

@coderabbitai

coderabbitai Bot commented Aug 24, 2026

Copy link
Copy Markdown

Important

  • 🔍 Trigger review

This repository does not receive automatic reviews because it has fewer than 10 stars.

⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: ef7781ca-17c1-49a2-a827-1a96ec7b1b66


Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@woneeeee woneeeee changed the title Fix/wth 468 백엔드 패널티 관리 UI 변경에 따른 수정사항 반영 [WTH-468] 백엔드 패널티 관리 UI 변경에 따른 수정사항 반영 Aug 24, 2026
@woneeeee woneeeee self-assigned this Aug 24, 2026
@woneeeee woneeeee added the 📬 API 서버 API 통신 label Aug 24, 2026
@woneeeee
woneeeee requested review from hyxklee and soo0711 August 24, 2026 08:15

@hyxklee hyxklee left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

우선 어드민쪽 작업 먼저 리뷰를 했습니당
관련된 리뷰를 보시고, 일단 이번 PR에서 유저쪽은 다음 이슈로 분리해서 진행하는게 좋을 것 같아용

어드민 쪽에 달린 PR이 유저 쪽에서도 해당하는 경우가 많을테니 함께 수정하면서 작업을 해주시면 될 것 같습니당

고생하셨어요! 처음인데 크게 벗어나는 것 없이 작업이 되었네용

var backgroundImageStorageKey: String? = backgroundImageStorageKey
private set

@Column(nullable = false)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

DB 변경시 resources/db/migration에 마이그레이션 쿼리가 필요합니당
Flyway에 대해서 한 번 Claude와 함께 이야기해보고, 이해한 후에 추가해주세용

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.

아 확인했습니당 V12__add_club_warning_and_penalty_rule.sql로 파일 추가했습니다!

var warningEnabled: Boolean = false
private set

@Column(length = 500)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

nullable 옵션도 넣어주면 조을 것 같아용

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.

넹 !! @Column(length = 500, nullable = true)로 수정했습니다!

@RequestParam(required = false) cardinalNumber: Int?,
@RequestParam(defaultValue = "0") page: Int,
@RequestParam(defaultValue = "20") size: Int,
): CommonResponse<PageResponse<ClubMemberResponse>> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

검색의 경우는 Pagination이 굳이 없어도 될 것 같아요!
추가하신 별도의 의도가 있을까요??

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.

image

해당 멤버 리스트에서 검색 결과가 보여지다보니 검색 결과가 많을 경우를 생각해서 페이지네이션으로 제작해두었는데 생각해보니 이름으로 검색하면 동명이인이 7명까진 없을 것 같네요... 페이지네이션은 제거해두겠습니당!!

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

검색이 아 음 아래 화면도 같이 동기화가 되는군요..
음.... 검색을 페이지네이션으로 주는 케이스가 보편적으로 있는지 한 번 봐야할 것 같고, 효율적으로 위아래 화면을 동기화하는 방법이 뭔지 고민해보면 좋을 것 같네요!

image

val score: Int = 1,
@field:Schema(description = "페널티 사유", example = "정기모임 무단 불참")
@field:NotBlank
val penaltyDescription: String,

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

사유가 디자인 스펙에는 몇 자인지 안 나타나있는 것 같네용 한 번 확인해서 반영해주세요!

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.

피그마 코멘트로 한 번 여쭤보긴 했는데 일단 임의로 50자로 제한해두었습니닷 추후 스펙이 확정되면 수정하겠습니다!

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.

디자인 상으로는 20자가 적합할 것 같다고 하셔서 20자로 수정해두었습니다!


val penalty = mapper.toEntity(request, clubMember, cardinal)
penaltyRepository.save(penalty)
request.userIds.forEach { targetUserId ->

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이렇게 내부에서 forEach를 통해 db에서 조회를 하게 되면 request.userIds 만큼 쿼리가 날라갑니당
해당 유스케이스의 경우에는 많은 인원을 선택해서 들어오는 경우는 적을 것 같긴 하지만, 이런 점을 인지하고 BE 개발을 하시면 좋습니당

BE는 항상 DB와 엮여있기 때문이에오

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.

넵 감사합니다!! 혹시 현재 코드에서 bulk 조회 방식으로 개선이 필요할까용..?? 아니면 이번 PR에서는 인지하는 선에서 넘어가면 될까요??

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

우선 주석 정도로 남겨놓고, 학습 후에 다음에 리팩토링을 하는 것으로 합시당

clubMemberRepository.findByIdWithLock(clubMember.id)
?: throw PenaltyNotFoundException()
lockedMember.incrementPenaltyCount()
if (request.penaltyType == PenaltyType.WARNING && !clubMember.club.warningEnabled) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

반복문 내부에서 경고 활성화 여부를 확인하고 있는 것 같네용
이렇게 되면 clubMember.club을 가져오면서 club을 조회하는 SELECT 문이 반복해서 나가게될 것 같아요 (FetchJoin을 이용하면 clubMember를 조회할 때 club도 같이 조회할 수 있습니다. 이건 FetchJoin, N+1 문제를 찾아보시면 좋아요)

디자인 스펙을 보면 경고/패널티는 한 번의 요청에서 섞이지 않는 것으로 나옵니당 WARGING인 경우는 반복문 밖 (위)에서 처리해주는게 좋아보여요

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.

아하... 넵! 그럼 clubReader를 통해서 반복문 진입 전에 warningEnabled를 한 번만 확인하도록 수정해두겠습니당!

class GetPenaltyRuleQueryService(
private val clubReader: ClubReader,
) {
@Transactional(readOnly = true)

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

쿼리 서비스에서는 클래스에 @transactional(readOnly = true)를 다는 것이 컨벤션입니당
클로드가 못잡아줬나보네요..

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

하네스 업데이트를 해야하나

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.

앗 넵 !! 클래스 레벨로 이동했습니다! 하네스도 같이 업데이트해두었는데 한 번 확인 부탁드립니당!

import org.springframework.web.bind.annotation.RestController

@Tag(name = "PENALTY", description = "패널티 API")
@Tag(name = "PENALTY", description = "페널티 API")

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

이번 작업은 어드민쪽 작업까지로 분리하고, 유저쪽은 PR을 나눠서 작업하면 좋을 것 같아용
BE는 FE에 비해 비교적 코드 양이 적기 때문에 500줄 정도면 큰 편으로 생각합니당

베스트는 300줄 이내, 큰 작업의 경우는 500줄 정도로 PR 볼륨을 잡고 작업을 분리하면서 하시면 좋을 것 같아요!

@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.

넵 !! 유저쪽은 별도 PR로 분리하겠습니다! 그럼 현재 브랜치에서 유저 마이페이지 관련 파일들은 되돌리고, 지금 wth-468 어드민 브랜치를 베이스로 한 새 브랜치에 유저 코드만 체리 픽해 PR로 올리고, 어드민 PR이 dev에 머지되면 유저 피알 base를 dev로 변경하겠습니당!!

@woneeeee
woneeeee requested a review from hyxklee August 25, 2026 05:20

@soo0711 soo0711 left a comment

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.

수고하셨습니다!! 👍 👍 처음인데도 백엔드 코드 엄청 잘 구현한 것 같아요 ㅎㅎ 짱짱
아래에 몇가지 피드백 남겼습니당

Comment on lines +1 to +3
ALTER TABLE club
ADD COLUMN warning_enabled BOOLEAN NOT NULL DEFAULT FALSE,
ADD COLUMN penalty_rule VARCHAR(500);

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.

club_member : warning_count 필드랑 penalty : score 필드도 추가되어야할 것 같아욤!

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.

앗 구러네요 반영해두겠습니다!! 감사합니당

Comment on lines +10 to +12
class GetPenaltyRuleQueryService(
private val clubReader: ClubReader,
) {

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.

이건 컨트롤러에서 사용 되지 않는 것 같은데 요건 어느 곳에서 사용하는 건가용?!

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.

아 원래 해당 피알에서 유저용 api까지 같이 반영하고 다시 유저용 api 따로 분리하다가 해당 부분이 아직 어드민에 남아있는 것 같아요!! 유저 페널티 규칙 조회 API에서 사용하고 있어서 #98 요기서 확인해보시면 될 것 같아요!!

Comment on lines +9 to 10
@field:Schema(description = "수정할 페널티 사유 (null=변경 안 함)", example = "정기모임 무단 불참 (수정)", nullable = true)
val penaltyDescription: String?,

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.

@field:Size(max = 20) 수정할 때도 20 글자수 제한 들어가야할 것 가타요!

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.

헙 그러네요... 꼼꼼한 리뷰 감사함니다,, 생각도 못하고 있었어요

@woneeeee
woneeeee merged commit 3d0f54d into dev Aug 25, 2026
2 checks passed
@woneeeee
woneeeee deleted the fix/WTH-468-백엔드-패널티-관리-UI-변경에-따른-수정사항-반영 branch August 25, 2026 14:27
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

📬 API 서버 API 통신

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants