Skip to content

[WTH-491] 동아리 멤버 커스텀 포지션 + 벌크 변경 + 검색 개선 - #113

Merged
woneeeee merged 10 commits into
devfrom
feat/WTH-491-커스텀-포지션-api-구현
Sep 21, 2026

Hidden character warning

The head ref may contain hidden characters: "feat/WTH-491-\ucee4\uc2a4\ud140-\ud3ec\uc9c0\uc158-api-\uad6c\ud604"
Merged

woneeeee merged 10 commits into
devfrom
feat/WTH-491-커스텀-포지션-api-구현

Conversation

@woneeeee

@woneeeee woneeeee commented Sep 21, 2026

Copy link
Copy Markdown
Member

📌 Summary

동아리 멤버에게 커스텀 포지션(기획/프론트엔드/백엔드 등)을 지정하는 기능을 중심으로, 관련해서 나온 벌크 변경/검색 개선/학과 목록 보정을 함께 담았습니다.

📝 Changes

What

  1. 커스텀 포지션 지정 기능: 동아리 관리자가 포지션 옵션(이름/색상/순서, 최대 6개)을 정의하고, 멤버별로 포지션을 지정/해제할 수 있는 API 추가
  2. 멤버 포지션 일괄 변경 API: 여러 멤버를 선택해 동일한 포지션을 한 번에 지정/해제하는 벌크 API 추가
  3. 멤버 검색 역할 필터 개선: GET .../members/searchmemberRole 파라미터를 제거하고, keyword가 역할 라벨("부원"/"리더"/"운영진")과 정확히 일치하면 이름/학과/학번 대신 역할로 필터링하도록 통합
  4. 학과 목록 보정: 커리어넷 오픈API 학과 목록에 아직 없는 "인공지능학과"를 이름 기준 중복 없이 보정 추가

Why

  • 포지션 기능: 어드민 멤버 목록에서 멤버별 담당 역할(기획/프론트/백엔드 등)을 표시·관리할 필요가 있었음
  • 벌크 변경: 멤버를 한 명씩 포지션 지정하는 게 비효율적이라 여러 명 한 번에 처리하는 플로우가 필요했음
  • 검색 개선: 프론트에서 역할도 통합 검색창(keyword)으로 찾는 게 자연스러운 UX였음
  • 학과 목록: 커리어넷 API에 신설 학과가 아직 반영되지 않아 수동 보정 필요

How

  • 포지션: ClubPositionOption 엔티티(동아리당 최대 6개) + ClubMember.assignPosition()로 지정/해제, N+1 방지를 위해 페이지 내 옵션을 일괄 조회 후 매핑
  • 벌크 변경: 기존 applyOb 패턴과 동일하게 대상 멤버를 정렬된 id 순서로 PESSIMISTIC_WRITE 락 후 일괄 처리, 하나라도 실패하면 전체 롤백(부분 반영 없음)
  • 검색: GetClubMemberQueryService.searchClubMembers에서 keyword를 역할 라벨 맵과 먼저 비교해 일치하면 memberRole 필터로, 아니면 기존 LIKE 검색으로 위임
  • 학과 목록: GetUniversityQueryService.getMajors()에서 외부 API 응답에 없을 때만 하드코딩된 항목을 추가(이름 기준 dedupe), 정렬/캐싱 로직은 그대로 유지

📸 Screenshots / Logs

없음 (API 변경, 스크린샷 대상 아님)

💡 Reviewer 참고사항

  • 4개 커밋으로 기능별 분리되어 있어 커밋 단위로 리뷰 가능합니다.
  • 기존 PATCH .../members/searchmemberRole 파라미터를 제거하는 breaking change가 포함되어 있습니다 (프론트 연동 확인 필요).

✅ Checklist

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

외부 API(커리어넷)에 아직 등록되지 않은 인공지능학과를 이름 기준
중복 없이 목록에 포함시킨다.
동아리 관리자가 포지션 옵션(이름/색상/순서, 최대 6개)을 정의하고
멤버별로 포지션을 지정/해제할 수 있는 API를 추가한다.
관리자가 여러 멤버를 선택해 동일한 포지션을 한 번에 지정하거나
해제할 수 있도록 벌크 액션 API를 추가한다. applyOb와 동일하게
정렬된 id 순서로 락을 걸어 데드락을 방지하고, 대상 중 하나라도
실패하면 전체를 롤백한다.
GET .../members/search의 memberRole 파라미터를 제거하고,
keyword가 역할 라벨("부원"/"리더"/"운영진")과 정확히 일치하면
이름/학과/학번 검색 대신 해당 역할로 필터링하도록 통합한다.
페이지네이션 목록(.../members)의 memberRole 드롭다운 필터는
그대로 유지한다.
@coderabbitai

coderabbitai Bot commented Sep 21, 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: Advanced

Run ID: 77f27398-ddb3-4c4d-a361-89ede5c4d397


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 requested review from hyxklee and soo0711 and removed request for soo0711 September 21, 2026 13:45
@woneeeee woneeeee self-assigned this Sep 21, 2026
@woneeeee woneeeee added the 📬 API 서버 API 통신 label Sep 21, 2026
@field:Schema(description = "포지션 색상 hex 값 (#RRGGBB)", example = "#4CAF50")
@field:NotBlank
@field:Pattern(regexp = "^#[0-9A-Fa-f]{6}$", message = "색상은 #RRGGBB 형식의 hex 값이어야 합니다.")
val colorHex: 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.

색상 이렇게 처리하기로 FE랑 얘기 된거졍??

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.

프론트분이랑 다시 얘기하면서 다르게 수정햇는데 다시 한 번 확인 부탁드립니닷!1

@ExplainError("생성하려는 동아리가 이미 있는 경우 발생합니다. 동아리 중복은 동일한 학교 안에 동일한 이름의 동아리가 있는지 검증합니다.")
DUPLICATE_CLUB(21119, HttpStatus.CONFLICT, "이미 존재하는 동아리입니다."),

@ExplainError("포지션 옵션을 동아리당 최대 개수(6개)를 초과하여 저장하려 할 때 발생합니다.")

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.

6개를 string에 하드코딩으로 하기엔 확장성을 너무 닫는 느낌이니까 빼고, 아래 예외 메시지도 MAX_OPTION_COUNT를 보여주도록 합시다

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.

넵 반영했습니다! MAX_OPTION_COUNT 상수를 참조하도록 메시지를 "포지션 옵션은 최대 ${ClubPositionOption.MAX_OPTION_COUNT}개까지 설정할 수 있습니다."로 수정했습니닷!!

*/
@Entity
@Table(name = "club_position_option")
class ClubPositionOption(

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.

그냥 club entity 안에 json 필드로 넣는게 나을까 싶기도 하고... 이정도 정보까지 정규화를 해야하나 싶기두 하네용 한 번 클로드와 논의를...

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.

포지션 이름/색상/순서를 개별적으로 수정(이름 변경, 순서 재배치)해야 하고, 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, "포지션 옵션이 저장되었습니다."),

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.

11128이 왜 없징

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.

오잉 그러네요 11128부터 되도록 수정해두겟습니다!

* 커리어넷 오픈API에 아직 등록되지 않은 신설 학과("인공지능학과")를 보정 추가한다.
* API가 이미 해당 학과를 포함하게 되더라도 이름 기준으로 중복 추가되지 않는다.
*/
@Cacheable(value = ["majors"], key = "'all'")

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.

이렇게 넣어줘도 redis cache에 잘 들어가나요?

@woneeeee woneeeee Sep 21, 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.

넹 정상 동작한다고 합니닷...

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

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.

아닌가 정규화 해야 이름 변경에 유연하게 대처할 수 있겠다 싶기도 하네ㅛㅇㅇ

{ selector(it) },
)

companion object {

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.

이렇게 넣어주기 보다는 환경변수에서 list로 넣어줄 수 있게 해주는게, 앞으로 수동으로 추가할 학과들이 늘어나는 경우도 대응할 수 있을 것 같아요

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.

아하 좋습니닷!

UniversityProperties(@ConfigurationProperties(prefix = "university"))를 추가해서 application.ymluniversity.manual-majors에서 리스트로 관리하도록 바꿨슴니닷!!

woneeeee and others added 6 commits September 21, 2026 23:22
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>
@woneeeee
woneeeee merged commit 31b5bc4 into dev Sep 21, 2026
2 checks passed
@woneeeee
woneeeee deleted the feat/WTH-491-커스텀-포지션-api-구현 branch September 21, 2026 14:30
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.

2 participants