[WTH-491] 동아리 멤버 커스텀 포지션 + 벌크 변경 + 검색 개선 - #113
Hidden character warning
Conversation
외부 API(커리어넷)에 아직 등록되지 않은 인공지능학과를 이름 기준 중복 없이 목록에 포함시킨다.
동아리 관리자가 포지션 옵션(이름/색상/순서, 최대 6개)을 정의하고 멤버별로 포지션을 지정/해제할 수 있는 API를 추가한다.
관리자가 여러 멤버를 선택해 동일한 포지션을 한 번에 지정하거나 해제할 수 있도록 벌크 액션 API를 추가한다. applyOb와 동일하게 정렬된 id 순서로 락을 걸어 데드락을 방지하고, 대상 중 하나라도 실패하면 전체를 롤백한다.
GET .../members/search의 memberRole 파라미터를 제거하고,
keyword가 역할 라벨("부원"/"리더"/"운영진")과 정확히 일치하면
이름/학과/학번 검색 대신 해당 역할로 필터링하도록 통합한다.
페이지네이션 목록(.../members)의 memberRole 드롭다운 필터는
그대로 유지한다.
|
Important
This repository does not receive automatic reviews because it has fewer than 10 stars. ⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Advanced Run ID: 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. Comment |
| @field:Schema(description = "포지션 색상 hex 값 (#RRGGBB)", example = "#4CAF50") | ||
| @field:NotBlank | ||
| @field:Pattern(regexp = "^#[0-9A-Fa-f]{6}$", message = "색상은 #RRGGBB 형식의 hex 값이어야 합니다.") | ||
| val colorHex: String, |
There was a problem hiding this comment.
프론트분이랑 다시 얘기하면서 다르게 수정햇는데 다시 한 번 확인 부탁드립니닷!1
| @ExplainError("생성하려는 동아리가 이미 있는 경우 발생합니다. 동아리 중복은 동일한 학교 안에 동일한 이름의 동아리가 있는지 검증합니다.") | ||
| DUPLICATE_CLUB(21119, HttpStatus.CONFLICT, "이미 존재하는 동아리입니다."), | ||
|
|
||
| @ExplainError("포지션 옵션을 동아리당 최대 개수(6개)를 초과하여 저장하려 할 때 발생합니다.") |
There was a problem hiding this comment.
6개를 string에 하드코딩으로 하기엔 확장성을 너무 닫는 느낌이니까 빼고, 아래 예외 메시지도 MAX_OPTION_COUNT를 보여주도록 합시다
There was a problem hiding this comment.
넵 반영했습니다! MAX_OPTION_COUNT 상수를 참조하도록 메시지를 "포지션 옵션은 최대 ${ClubPositionOption.MAX_OPTION_COUNT}개까지 설정할 수 있습니다."로 수정했습니닷!!
| */ | ||
| @Entity | ||
| @Table(name = "club_position_option") | ||
| class ClubPositionOption( |
There was a problem hiding this comment.
그냥 club entity 안에 json 필드로 넣는게 나을까 싶기도 하고... 이정도 정보까지 정규화를 해야하나 싶기두 하네용 한 번 클로드와 논의를...
There was a problem hiding this comment.
포지션 이름/색상/순서를 개별적으로 수정(이름 변경, 순서 재배치)해야 하고, club_member.position_option_id로 FK 참조를 걸어서 무결성도 보장해야 하는데, JSON 필드로 넣으면 이 부분을 다 애플리케이션 레벨에서 파싱/검증해야 해서 오히려 복잡해질 것 같아요. 지금 구조(별도 엔티티 + FK) 그대로 유지하겠습니다.
클로드랑 한 번 얘기해봤는데 이렇게 말해줬습니닷... 일단 요대로 유지하는걸로 하는게 좋을 것 같아용!!
| MEMBER_LIST_FIND_SUCCESS(11124, HttpStatus.OK, "멤버 목록을 성공적으로 조회했습니다."), | ||
| MEMBER_DETAIL_FIND_SUCCESS(11125, HttpStatus.OK, "멤버 상세 정보를 성공적으로 조회했습니다."), | ||
| MEMBER_POSTS_FIND_SUCCESS(11126, HttpStatus.OK, "멤버 게시글 목록을 성공적으로 조회했습니다."), | ||
| POSITION_OPTIONS_SAVED_SUCCESS(11127, HttpStatus.OK, "포지션 옵션이 저장되었습니다."), |
There was a problem hiding this comment.
오잉 그러네요 11128부터 되도록 수정해두겟습니다!
| * 커리어넷 오픈API에 아직 등록되지 않은 신설 학과("인공지능학과")를 보정 추가한다. | ||
| * API가 이미 해당 학과를 포함하게 되더라도 이름 기준으로 중복 추가되지 않는다. | ||
| */ | ||
| @Cacheable(value = ["majors"], key = "'all'") |
There was a problem hiding this comment.
넹 정상 동작한다고 합니닷...
@Cacheable은 메서드 실행 결과(리턴값)를 캐싱하는 거라서,
getMajors() 안에서 수동 추가(withManualAdditions)를 끝낸 이후의
최종 List<MajorResponse>가 캐시에 들어갑니다.
Port가 반환한 원본을 캐싱하는 게 아니에요.
MajorResponse도 평범한 Kotlin data class라
GenericJackson2JsonRedisSerializer로 직렬화하는 데 문제없고,
이 동작 자체를 검증하는 통합테스트(UniversityCacheIntegrationTest)도 이미 있습니다.
클로드한테도 확인 받았습니닷...
| ON club_position_option (club_id); | ||
|
|
||
| ALTER TABLE club_member | ||
| ADD COLUMN position_option_id BIGINT NULL, |
There was a problem hiding this comment.
아닌가 정규화 해야 이름 변경에 유연하게 대처할 수 있겠다 싶기도 하네ㅛㅇㅇ
| { selector(it) }, | ||
| ) | ||
|
|
||
| companion object { |
There was a problem hiding this comment.
이렇게 넣어주기 보다는 환경변수에서 list로 넣어줄 수 있게 해주는게, 앞으로 수동으로 추가할 학과들이 늘어나는 경우도 대응할 수 있을 것 같아요
There was a problem hiding this comment.
아하 좋습니닷!
UniversityProperties(@ConfigurationProperties(prefix = "university"))를 추가해서 application.yml의 university.manual-majors에서 리스트로 관리하도록 바꿨슴니닷!!
MAX_OPTION_COUNT 변경 시 예외 메시지가 자동으로 반영되도록 상수를 참조하게 한다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
11127 다음이 11129로 건너뛰어 번호가 비어 있던 것을 재정렬한다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
커리어넷 API 미등록 학과 보정 목록이 코드에 하드코딩되어 있어 학과 추가/삭제 시 재배포가 필요했다. UniversityProperties로 외부화해 application.yml만 수정해도 반영되게 한다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
프론트가 hex 코드(#RRGGBB) 대신 PRIMARY/SECONDARY/PURPLE/PINK/ CAUTION/ERROR 같은 프리셋 이름으로 색상을 지정하도록 요청이 와서, ClubPositionOption의 색상 필드를 PositionColor enum으로 바꾸고 관련 hex 검증 VO(ColorHex)를 제거한다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
동아리 멤버 목록/상세 조회에 positionOptionId 필터를 추가하고, 멤버 응답(public/detail)에 지정된 포지션 정보를 함께 내려준다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
인공지능학과 보정 추가로 목업 학과 20개가 21개로 반환되는데 테스트는 여전히 20개를 기대해 CI가 실패하고 있었다. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
📌 Summary
동아리 멤버에게 커스텀 포지션(기획/프론트엔드/백엔드 등)을 지정하는 기능을 중심으로, 관련해서 나온 벌크 변경/검색 개선/학과 목록 보정을 함께 담았습니다.
📝 Changes
What
GET .../members/search의memberRole파라미터를 제거하고,keyword가 역할 라벨("부원"/"리더"/"운영진")과 정확히 일치하면 이름/학과/학번 대신 역할로 필터링하도록 통합Why
How
ClubPositionOption엔티티(동아리당 최대 6개) +ClubMember.assignPosition()로 지정/해제, N+1 방지를 위해 페이지 내 옵션을 일괄 조회 후 매핑applyOb패턴과 동일하게 대상 멤버를 정렬된 id 순서로PESSIMISTIC_WRITE락 후 일괄 처리, 하나라도 실패하면 전체 롤백(부분 반영 없음)GetClubMemberQueryService.searchClubMembers에서 keyword를 역할 라벨 맵과 먼저 비교해 일치하면memberRole필터로, 아니면 기존 LIKE 검색으로 위임GetUniversityQueryService.getMajors()에서 외부 API 응답에 없을 때만 하드코딩된 항목을 추가(이름 기준 dedupe), 정렬/캐싱 로직은 그대로 유지📸 Screenshots / Logs
없음 (API 변경, 스크린샷 대상 아님)
💡 Reviewer 참고사항
PATCH .../members/search의memberRole파라미터를 제거하는 breaking change가 포함되어 있습니다 (프론트 연동 확인 필요).✅ Checklist