Skip to content

Feat/#38 조직 맴버 관리 - 조직 맴버 권한 변경#43

Merged
kingmingyu merged 11 commits intodevelopfrom
feat/#38
Feb 21, 2026
Merged

Feat/#38 조직 맴버 관리 - 조직 맴버 권한 변경#43
kingmingyu merged 11 commits intodevelopfrom
feat/#38

Conversation

@kingmingyu
Copy link
Collaborator

@kingmingyu kingmingyu commented Feb 19, 2026

📌 관련 이슈

🚀 개요

이번 PR에서 변경된 핵심 내용을 요약해주세요.
API를 요청한 사용자의 권한을 확인하고(ADMIN인 경우만 실행 가능) 조직 id와 해당 맴버의 id, 바꾸려고 하는 역할을 입력받아 권한을 변경합니다.

📄 작업 내용

구체적인 작업 내용을 설명해주세요.

  • 요청한 사용자의 권한을 확인
  • 권한을 변경하려는 사용자의 역할을 확인 (ADMIN이라면 변경 불가)
  • 권한 변경

📸 스크린샷 / 테스트 결과 (선택)

결과물 확인을 위한 사진이나 테스트 로그를 첨부해주세요.

  • 변경 전
image
  • 실행
image image image
  • ADMIN 역할이 2명 이하인 경우(항상 ADMIN은 2명 이상)
image
  • ADMIN 권한이 없는 경우
image

✅ 체크리스트

  • 브랜치 전략(GitHub Flow)을 준수했나요?
  • 메서드 단위로 코드가 잘 쪼개져 있나요?
  • 테스트 통과 확인
  • 서버 실행 확인
  • API 동작 확인

🔍 리뷰 포인트 (Review Points)

리뷰어가 중점적으로 확인했으면 하는 부분을 적어주세요. (P1~P4 적용 가이드)

  • 여기도 마찬가지로 ADMIN을 MEMBER로 바꿀 수 없게 막아놨습니다. 근데 그렇게 될 경우에 한 번 ADMIN은 영원한 ADMIN이 되어 버리는데 괜찮을까요?

💬 리뷰어 가이드 (P-Rules)
P1: 필수 반영 (Critical) - 버그 가능성, 컨벤션 위반. 해결 전 머지 불가.
P2: 적극 권장 (Recommended) - 더 나은 대안 제시. 가급적 반영 권장.
P3: 제안 (Suggestion) - 아이디어 공유. 반영 여부는 드라이버 자율.
P4: 단순 확인/칭찬 (Nit) - 사소한 오타, 칭찬 등 피드백.

Summary by CodeRabbit

  • 새로운 기능
    • 조직 내 구성원 역할(ADMIN/MEMBER) 업데이트 기능 추가
    • 역할 변경용 PATCH API 추가 및 갱신된 구성원 정보 반환
  • 권한/검증
    • 요청자는 조직 관리자만 역할 변경 가능하도록 권한 검사 적용
    • 본인 역할 변경 시도 차단 및 마지막 ADMIN 강등 불가에 대한 명확한 오류 응답 추가
  • 문서
    • 역할 변경 엔드포인트에 대한 API 문서 추가

…ckend into feat/#38

# Conflicts:
#	src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java
#	src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java
#	src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java
#	src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java
#	src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java
@kingmingyu kingmingyu self-assigned this Feb 19, 2026
@kingmingyu kingmingyu added the ✨ Feature 새로운 기능 추가 label Feb 19, 2026
@kingmingyu kingmingyu linked an issue Feb 19, 2026 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link

coderabbitai bot commented Feb 19, 2026

Walkthrough

조직 멤버의 역할 변경 기능을 추가합니다. PATCH 요청으로 조직·요청자·대상 멤버를 검증(권한·존재·마지막 ADMIN 검사)한 뒤 엔티티 역할을 갱신하고 변경된 멤버 DTO를 반환합니다.

Changes

Cohort / File(s) Summary
요청/DTO
src/main/java/.../OrgRequest.java
UpdateRole 레코드 추가(필드: OrgRole orgRole, @NotNull, @Schema 주석).
컨버터/DTO 매핑
src/main/java/.../OrgConverter.java
toOrgMemberDTO(OrgMember) 헬퍼 추가 및 슬라이스 변환에서 재사용(refactor).
서비스 인터페이스/구현
src/main/java/.../OrgService.java, src/main/java/.../OrgServiceImpl.java
updateOrgMembersRole(...) 메서드 추가: 자기 변경 금지, 조직/요청자 존재·ADMIN 권한 검사, 대상 멤버 조회, 마지막 ADMIN 방지 로직, 엔티티 역할 갱신 후 DTO 반환.
엔티티/리포지토리
src/main/java/.../OrgMember.java, src/main/java/.../OrgMemberRepository.java
OrgMember.updateRole(OrgRole) 뮤테이터 추가. findByUserIdAndOrgId(...) 복원(활성 사용자 조건) 및 countByOrganizationIdAndRole(...) 추가.
컨트롤러/문서
src/main/java/.../OrgController.java, src/main/java/.../OrgControllerDocs.java
PATCH /api/org/members/{orgId}/{memberId} 엔드포인트 및 Swagger 문서 추가. 요청 바디는 OrgRequest.UpdateRole.
예외 코드
src/main/java/.../OrgErrorCode.java
새 에러 코드 추가: ORG_CANNOT_ROLE_CHANGE_SELF, ORG_LAST_ADMIN, ORG_INVITATION_FORBIDDEN_USER, ORG_MEMBER_ALREADY_ACTIVE 및 일부 항목 재배치/정리.

Sequence Diagram

sequenceDiagram
    participant Client as "Client"
    participant Controller as "OrgController"
    participant Service as "OrgServiceImpl"
    participant Repo as "OrgMemberRepository"
    participant Entity as "OrgMember"

    Client->>Controller: PATCH /api/org/members/{orgId}/{memberId}\nBody: UpdateRole{orgRole}
    Controller->>Service: updateOrgMembersRole(userId, orgId, memberId, dto)
    Service->>Repo: findByOrgIdAndId(orgId) / find Organization
    Repo-->>Service: Organization
    Service->>Repo: findByUserIdAndOrgId(userId, orgId)
    Repo-->>Service: OrgMember (requester)
    Note over Service: 요청자 역할 검사 (ADMIN 여부)
    Service->>Repo: findByOrgIdAndId(orgId, memberId)
    Repo-->>Service: OrgMember (target)
    Note over Service: 마지막 ADMIN 검사 (countByOrganizationIdAndRole)
    Service->>Entity: orgMember.updateRole(newRole)
    Entity-->>Service: 변경 반영 (dirty-checking)
    Service->>Service: OrgConverter.toOrgMemberDTO(updated)
    Service-->>Controller: OrgMemberDTO
    Controller-->>Client: 200 DataResponse<OrgMemberDTO>
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Possibly related PRs

Suggested reviewers

  • ojy0903
  • jinnieusLab
🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 42.31% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Title check ✅ Passed PR 제목은 조직 멤버 권한 변경 기능을 명확하게 설명하며, 변경사항의 핵심을 잘 반영합니다.
Description check ✅ Passed PR 설명이 템플릿 구조를 따르고, 개요, 작업 내용, 테스트 결과 스크린샷, 체크리스트, 리뷰 포인트를 모두 포함하고 있습니다.
Linked Issues check ✅ Passed PR은 #38 이슈의 모든 요구사항을 충족합니다: 사용자 권한 확인, ADMIN 권한 필수 검증, 멤버 권한 변경 구현, 그리고 ADMIN 최소 2명 유지 로직 포함.
Out of Scope Changes check ✅ Passed 변경사항이 모두 멤버 권한 변경 기능 구현 범위 내에 있으며, OrgMemberRepository의 findByUserIdAndOrgId 복원은 권한 검증에 필요한 범위 내 변경입니다.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
  • 📝 Generate docstrings (stacked PR)
  • 📝 Generate docstrings (commit on current branch)
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/#38

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 and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 5

🧹 Nitpick comments (2)
src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java (1)

43-48: JPQL enum 비교에 문자열 리터럴 대신 파라미터 바인딩을 사용하는 것을 권장합니다.

현재 om.user.status = 'ACTIVE' 처럼 enum 값을 문자열 리터럴로 직접 비교하고 있습니다. UserStatus.ACTIVE enum의 이름이 변경되면 컴파일 에러 없이 런타임에서 쿼리가 잘못 동작할 수 있습니다.

파라미터 방식으로 변경하면 타입 안전성이 확보됩니다:

♻️ 파라미터 바인딩으로 변경 제안
-    `@Query`("SELECT om FROM OrgMember om " +
-            "WHERE om.user.id = :userId " +
-            "AND om.organization.id = :orgId " +
-            "AND om.user.status = 'ACTIVE'")
-    Optional<OrgMember> findByUserIdAndOrgId(`@Param`("userId") Long userId, `@Param`("orgId") Long orgId);
+    `@Query`("SELECT om FROM OrgMember om " +
+            "WHERE om.user.id = :userId " +
+            "AND om.organization.id = :orgId " +
+            "AND om.user.status = :userStatus")
+    Optional<OrgMember> findByUserIdAndOrgId(`@Param`("userId") Long userId,
+                                              `@Param`("orgId") Long orgId,
+                                              `@Param`("userStatus") UserStatus userStatus);

호출부에서는 findByUserIdAndOrgId(userId, orgId, UserStatus.ACTIVE) 형태로 사용합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java`
around lines 43 - 48, Change the JPQL in OrgMemberRepository so it does not
compare the enum via the string literal 'ACTIVE'; update the query used by
findByUserIdAndOrgId (or add an overloaded method) to accept a UserStatus
parameter and bind it (e.g., "om.user.status = :status") and change the method
signature to include a UserStatus status parameter (call site will pass
UserStatus.ACTIVE). Ensure you reference the UserStatus enum and parameter name
(e.g., status) in the `@Param` annotation so om.user.status uses a typed parameter
instead of a hard-coded string.
src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java (1)

210-221: findByUserIdAndOrgId 결과에 JOIN FETCH 없이 toOrgMemberDTO 호출 시 추가 쿼리가 발생합니다.

findByUserIdAndOrgId JPQL은 om.user.status 조건으로 인해 user 테이블과 암묵적 JOIN을 하지만, JOIN FETCH가 없어서 user 엔티티를 실제로 로딩하지 않습니다.

이후 221번 라인에서 OrgConverter.toOrgMemberDTO(orgMember) 호출 시 orgMember.getUser().getName() 등으로 user에 접근하면 Hibernate가 별도 SELECT * FROM user WHERE id = ? 를 추가로 발생시킵니다.

이 메서드 전체로 보면 총 4번의 쿼리(조직 조회 1 + 요청자 조회 1 + 대상 멤버 조회 1 + user 지연 로딩 1)가 발생합니다.

대상 조회 쿼리에 JOIN FETCH를 추가하거나, 이 용도에 특화된 별도 쿼리 메서드를 추가하는 것을 권장합니다:

♻️ JOIN FETCH 추가 예시 (OrgMemberRepository)
// 권한 변경 응답용: user 정보도 함께 페치
`@Query`("SELECT om FROM OrgMember om " +
        "JOIN FETCH om.user u " +
        "WHERE om.user.id = :userId " +
        "AND om.organization.id = :orgId " +
        "AND u.status = :userStatus")
Optional<OrgMember> findByUserIdAndOrgIdWithUser(`@Param`("userId") Long userId,
                                                  `@Param`("orgId") Long orgId,
                                                  `@Param`("userStatus") UserStatus userStatus);

As per coding guidelines, "JPA 사용 시 N+1 문제나 불필요한 쿼리가 발생하지 않는지" 확인이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`
around lines 210 - 221, The current retrieval uses
orgMemberRepository.findByUserIdAndOrgId which does not fetch the associated
User, causing a lazy-load when OrgConverter.toOrgMemberDTO(orgMember) accesses
orgMember.getUser(); change the service to use a repository method that JOIN
FETCHes the user (e.g., add and call findByUserIdAndOrgIdWithUser on
OrgMemberRepository that joins FETCH om.user and filters by user status) or
create a dedicated query returning the OrgMember with user eagerly loaded, then
use that result for updateRole and OrgConverter.toOrgMemberDTO to avoid the
extra select.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`:
- Around line 197-199: The method updateOrgMembersRole in OrgServiceImpl fetches
Organization organization but never checks its status, allowing role changes on
soft-deleted orgs; add a check after retrieving organization: if
organization.getStatus() == OrgStatus.DELETED then throw new
OrgHandler(OrgErrorCode.ORG_SOFT_DELETED) (same pattern as
getOrganizationDetail) so updates are blocked for soft-deleted organizations.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java`:
- Line 144: Update the ApiResponse for the ADMIN→MEMBER change in
OrgControllerDocs.java: replace the incorrect responseCode "401" with "400" and
adjust the description accordingly (the annotation on the ApiResponse for the
business-rule rejection in OrgControllerDocs should reflect Bad Request); locate
the ApiResponse entry in the OrgControllerDocs class that currently reads
responseCode = "401" and change it to responseCode = "400" so it matches the
ORG_400_4 error and actual response payload.
- Line 143: The `@ApiResponse` description for responseCode "200" in
OrgControllerDocs is incorrectly copied from getOrgMembersCount; update the
description for the 권한 변경 API to reflect that it returns the changed member info
(OrgMemberDTO). Locate the `@ApiResponse`(...) annotation in OrgControllerDocs for
the permission-change endpoint and replace the text "성공 (totalCount: 전체 멤버 수)"
with a concise message such as "성공 (변경된 멤버 정보 반환: OrgMemberDTO)" or equivalent
that mentions OrgMemberDTO.
- Line 152: The UpdateRole request parameter is missing `@Valid` so Bean
Validation on OrgRequest.UpdateRole.orgRole (annotated `@NotNull`) won't run;
update the controller method signature that accepts OrgRequest.UpdateRole (the
parameter currently declared as "@RequestBody OrgRequest.UpdateRole dto") to use
"@RequestBody `@Valid` OrgRequest.UpdateRole dto" so Spring triggers validation
(mirror the existing approach used in createOrganization/modifyOrganization).

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java`:
- Around line 129-138: The updateOrgMembersRole controller is missing request
validation so OrgRequest.UpdateRole's `@NotNull` on orgRole isn't enforced; add
`@Valid` to the controller parameter (change the signature of updateOrgMembersRole
to accept `@RequestBody` `@Valid` OrgRequest.UpdateRole dto) so Spring performs
validation before calling orgService.updateOrgMembersRole and avoids null being
passed into orgMember.updateRole/OrgConverter.toOrgMemberDTO causing NPEs;
ensure any BindingResult or global exception handler already handles
MethodArgumentNotValidException appropriately.

---

Nitpick comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`:
- Around line 210-221: The current retrieval uses
orgMemberRepository.findByUserIdAndOrgId which does not fetch the associated
User, causing a lazy-load when OrgConverter.toOrgMemberDTO(orgMember) accesses
orgMember.getUser(); change the service to use a repository method that JOIN
FETCHes the user (e.g., add and call findByUserIdAndOrgIdWithUser on
OrgMemberRepository that joins FETCH om.user and filters by user status) or
create a dedicated query returning the OrgMember with user eagerly loaded, then
use that result for updateRole and OrgConverter.toOrgMemberDTO to avoid the
extra select.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java`:
- Around line 43-48: Change the JPQL in OrgMemberRepository so it does not
compare the enum via the string literal 'ACTIVE'; update the query used by
findByUserIdAndOrgId (or add an overloaded method) to accept a UserStatus
parameter and bind it (e.g., "om.user.status = :status") and change the method
signature to include a UserStatus status parameter (call site will pass
UserStatus.ACTIVE). Ensure you reference the UserStatus enum and parameter name
(e.g., status) in the `@Param` annotation so om.user.status uses a typed parameter
instead of a hard-coded string.

Comment on lines +197 to +199
// 1. 조직 존재 여부 확인
Organization organization = orgRepository.findById(orgId)
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND));
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

소프트 삭제된 조직에 대한 권한 변경이 가능합니다 — OrgStatus.DELETED 체크가 누락되었습니다.

Organization organization 변수를 할당했지만 이후 한 번도 사용하지 않습니다. 기존 getOrganizationDetail 메서드를 보면:

if (organization.getStatus() == OrgStatus.DELETED) {
    throw new OrgHandler(OrgErrorCode.ORG_SOFT_DELETED);
}

이 체크가 존재합니다. 현재 updateOrgMembersRole에서는 이 체크가 없어서 소프트 삭제된 조직(status = DELETED)에서도 멤버 권한이 변경 가능합니다.

🐛 소프트 삭제 체크 추가 제안
 Organization organization = orgRepository.findById(orgId)
         .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND));
+
+// Soft Delete 된 조직이면 예외처리
+if (organization.getStatus() == OrgStatus.DELETED) {
+    throw new OrgHandler(OrgErrorCode.ORG_SOFT_DELETED);
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`
around lines 197 - 199, The method updateOrgMembersRole in OrgServiceImpl
fetches Organization organization but never checks its status, allowing role
changes on soft-deleted orgs; add a check after retrieving organization: if
organization.getStatus() == OrgStatus.DELETED then throw new
OrgHandler(OrgErrorCode.ORG_SOFT_DELETED) (same pattern as
getOrganizationDetail) so updates are blocked for soft-deleted organizations.

description = "맴버 권한 변경을 요청한 유저의 권한이 ADMIN인 경우 실행이 가능합니다. memberId에 해당하는 맴버의 권한을 변경시킵니다."
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공 (totalCount: 전체 멤버 수)"),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

200 응답 설명이 잘못된 값으로 복붙되었습니다.

"성공 (totalCount: 전체 멤버 수)"getOrgMembersCount API의 설명을 그대로 가져온 것입니다. 권한 변경 API의 성공 응답에는 변경된 멤버 정보(OrgMemberDTO)가 반환되므로 설명을 수정해야 합니다.

📝 수정 제안
-@ApiResponse(responseCode = "200", description = "성공 (totalCount: 전체 멤버 수)"),
+@ApiResponse(responseCode = "200", description = "성공 (변경된 멤버 정보 반환)"),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@ApiResponse(responseCode = "200", description = "성공 (totalCount: 전체 멤버 )"),
`@ApiResponse`(responseCode = "200", description = "성공 (변경된 멤버 정보 반환)"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java`
at line 143, The `@ApiResponse` description for responseCode "200" in
OrgControllerDocs is incorrectly copied from getOrgMembersCount; update the
description for the 권한 변경 API to reflect that it returns the changed member info
(OrgMemberDTO). Locate the `@ApiResponse`(...) annotation in OrgControllerDocs for
the permission-change endpoint and replace the text "성공 (totalCount: 전체 멤버 수)"
with a concise message such as "성공 (변경된 멤버 정보 반환: OrgMemberDTO)" or equivalent
that mentions OrgMemberDTO.

)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공 (totalCount: 전체 멤버 수)"),
@ApiResponse(responseCode = "401", description = "잘못된 요청을 보낸 경우(ADMIN의 권한 변경)"),
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

잘못된 HTTP 상태 코드: 401이 아니라 400이 맞습니다.

HTTP 401 Unauthorized는 인증(Authentication) 실패를 의미합니다. ADMIN→MEMBER 변경을 거부하는 것은 비즈니스 로직 규칙 위반이므로 400 Bad Request가 올바릅니다. PR 스크린샷에서도 실제 에러 응답이 "status": "Bad Request" (ORG_400_4)임을 확인할 수 있습니다.

예시:

  • 401: 토큰이 없거나 만료된 경우 (인증 실패)
  • 400: ADMIN을 MEMBER로 변경하려는 잘못된 요청 (비즈니스 규칙 위반)
📝 수정 제안
-@ApiResponse(responseCode = "401", description = "잘못된 요청을 보낸 경우(ADMIN의 권한 변경)"),
+@ApiResponse(responseCode = "400", description = "잘못된 요청을 보낸 경우(ADMIN의 권한 변경)"),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
@ApiResponse(responseCode = "401", description = "잘못된 요청을 보낸 경우(ADMIN의 권한 변경)"),
`@ApiResponse`(responseCode = "400", description = "잘못된 요청을 보낸 경우(ADMIN의 권한 변경)"),
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java`
at line 144, Update the ApiResponse for the ADMIN→MEMBER change in
OrgControllerDocs.java: replace the incorrect responseCode "401" with "400" and
adjust the description accordingly (the annotation on the ApiResponse for the
business-rule rejection in OrgControllerDocs should reflect Bad Request); locate
the ApiResponse entry in the OrgControllerDocs class that currently reads
responseCode = "401" and change it to responseCode = "400" so it matches the
ORG_400_4 error and actual response payload.

@AuthenticationPrincipal(expression = "userId") Long userId,
@PathVariable Long orgId,
@PathVariable Long memberId,
@RequestBody OrgRequest.UpdateRole dto
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

@RequestBody@Valid가 누락되었습니다 — @NotNull 검증이 동작하지 않습니다.

AI 요약에 따르면 OrgRequest.UpdateRoleorgRole 필드에 @NotNull이 선언되어 있습니다. 그런데 @Valid가 없으면 Spring은 Bean Validation을 실행하지 않아서, orgRolenull인 요청이 그대로 서비스 레이어까지 내려갑니다. 이는 NPE나 예상치 못한 DB 오류로 이어질 수 있습니다.

다른 메서드들(예: Line 24 createOrganization, Line 62 modifyOrganization)은 모두 @RequestBody @Valid``를 사용하고 있습니다.

🛡️ 수정 제안
-@RequestBody OrgRequest.UpdateRole dto
+@RequestBody `@Valid` OrgRequest.UpdateRole dto
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java`
at line 152, The UpdateRole request parameter is missing `@Valid` so Bean
Validation on OrgRequest.UpdateRole.orgRole (annotated `@NotNull`) won't run;
update the controller method signature that accepts OrgRequest.UpdateRole (the
parameter currently declared as "@RequestBody OrgRequest.UpdateRole dto") to use
"@RequestBody `@Valid` OrgRequest.UpdateRole dto" so Spring triggers validation
(mirror the existing approach used in createOrganization/modifyOrganization).

@AuthenticationPrincipal(expression = "userId") Long userId,
@PathVariable Long orgId,
@PathVariable Long memberId,
@RequestBody OrgRequest.UpdateRole dto
Copy link
Collaborator

@ojy0903 ojy0903 Feb 19, 2026

Choose a reason for hiding this comment

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

P3: Controller 메서드에서 OrgRequest.UpdateRole 필드에 Valid 어노테이션 달아서 NotNull 검사 적용되게 해주면 좋을 것 같아요!

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

수정했습니다! 꼼꼼하게 봐주셔서 감사합니다!

Copy link
Collaborator

@ojy0903 ojy0903 left a comment

Choose a reason for hiding this comment

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

P4: 고생하셨어요! 제가 보기엔 ADMIN 인 사용자를 MEMBER 로 바꾸지 못하면 문제가 생길 수도 있을 것 같은데, 아예 OrgRole 에 CREATOR 같이 최고 권한 역할을 만드는게 좋을까요? 아니면 지금처럼 ADMIN 은 MEMBER 로 바꾸지 못하도록 하되 화면 UI 에서 사용자에게 경고를 주는 방식으로 하는 게 맞을까요? 지민님 의견도 들어봐야 될 듯 합니다...

@kingmingyu kingmingyu changed the title Feat/#38 Feat/#38 조직 맴버 관리 - 조직 맴버 권한 변경 Feb 19, 2026
@kingmingyu
Copy link
Collaborator Author

P4: 고생하셨어요! 제가 보기엔 ADMIN 인 사용자를 MEMBER 로 바꾸지 못하면 문제가 생길 수도 있을 것 같은데, 아예 OrgRole 에 CREATOR 같이 최고 권한 역할을 만드는게 좋을까요? 아니면 지금처럼 ADMIN 은 MEMBER 로 바꾸지 못하도록 하되 화면 UI 에서 사용자에게 경고를 주는 방식으로 하는 게 맞을까요? 지민님 의견도 들어봐야 될 듯 합니다...

저도 뭔가 계속 새로운 권한을 만들면 끝이 없을 것 같아서 고민 중이었습니다..! 최고 권한 역할도 좋습니다 뭔가 ADMIN도 강등할 수 있는 방법이 필요할 것 같긴 합니다!

Copy link
Collaborator

@jinnieusLab jinnieusLab left a comment

Choose a reason for hiding this comment

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

P3: 고생하셨습니다! 제 생각에도 ADMIN이 MEMBER 권한으로 변경될 수 없다면 문제가 있을 것 같습니다.. 해당 조직 내 ADMIN이 두 명 이상 존재한다면 ADMIN -> MEMBER를 가능하게 하는 건 어떨까요?

만약 추가로 더 구현한다면, 조직 내 ADMIN이 오직 한 명이면 관리자를 다른 이로 임명하게끔 구현해도 될 듯 합니다! (+ ADMIN 권한인 유저 삭제(추방)하는 것도 비슷하게 고민해볼 수 있을 것 같습니다..)

@ojy0903
Copy link
Collaborator

ojy0903 commented Feb 19, 2026

P3: 지민님 제시하신 것처럼 조직 내에 ADMIN 이 2명 이상이면 강등 가능하게 하는 건 어떨까요? 생각해보니 OrgRole 에 새로 역할 추가를 하면 다른 메서드에서도 수정해야할 점이 많이 생길 수 있을 것 같아서... 이렇게 진행하고 역할 세분화는 2차 MVP 에 진행해보는거 어떨까요

# Conflicts:
#	src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java
#	src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgService.java
#	src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java
#	src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java
#	src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java
Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (3)
src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java (1)

29-34: UpdateRole 레코드 깔끔하게 잘 작성했네요! 👍

Java 17 record 문법과 @NotNull, @Schema의 조합이 적절합니다. orgRolenull로 들어오는 케이스를 차단하고, OrgRole 타입 자체로 잘못된 enum 문자열도 Jackson이 400 에러로 처리하므로 입력 방어가 이중으로 잘 되어 있습니다.

한 가지 소소한 팁: UpdateRole 레코드 끝(}) 다음에 빈 줄이 없어서 Invite 레코드와 붙어 보입니다. 다른 레코드들처럼 구분을 위해 빈 줄 하나만 추가하면 가독성이 올라가요.

✨ 선택적 개선 제안
     ) {}
+
     public record Invite(
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java`
around lines 29 - 34, Add a single blank line after the closing brace of the
UpdateRole record so it is visually separated from the next Invite record; edit
around the UpdateRole and Invite record declarations (the UpdateRole record
closing "}" and the subsequent Invite record) to insert one empty line to
improve readability and match surrounding record spacing.
src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java (2)

129-138: [운영 설계 논의] ADMIN의 영구적 고착 문제 — 어떻게 처리할 건가요?

PR 토론에서 이미 언급된 부분이지만, 코드 레벨에서도 짚어드립니다.

현재 구현은 ADMIN → MEMBER 다운그레이드를 전면 차단합니다. 이 경우 발생하는 실제 문제:

  1. 조직의 유일한 ADMIN이 탈퇴하고 싶어도 스스로 MEMBER로 바꾸거나 다른 사람에게 권한 이양 후 다운그레이드할 방법이 없음
  2. 실수로 권한을 부여한 경우 되돌릴 수 없어 관리 운영이 경직

권장 접근법 (ojy0903님 제안과 일치):

다운그레이드 허용 조건: org 내 ADMIN 수 >= 2
→ 마지막 ADMIN은 보호, 여러 ADMIN 중 한 명은 다운그레이드 가능

서비스 레이어에서 아래처럼 처리할 수 있습니다:

// 예시 로직
long adminCount = orgMemberRepository.countByOrganizationAndRole(org, OrgRole.ADMIN);
if (adminCount <= 1 && targetMember.getRole() == OrgRole.ADMIN) {
    throw new OrgException(OrgErrorCode.ORG_LAST_ADMIN_CANNOT_DEMOTE);
}

현재 MVP 범위에서 새 에러코드(ORG_LAST_ADMIN_CANNOT_DEMOTE) 하나만 추가하면 CREATOR 롤 도입 없이도 해결됩니다. CREATOR 롤은 2차 MVP에서 고려하는 것이 좋겠습니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java`
around lines 129 - 138, The controller method updateOrgMembersRole calls
orgService.updateOrgMembersRole but the service currently blocks all
ADMIN→MEMBER demotions; change the service logic to allow demotion only when the
organization has at least 2 admins: in the service method check
orgMemberRepository.countByOrganizationAndRole(org, OrgRole.ADMIN) and if the
count <= 1 and the targetMember.getRole() == OrgRole.ADMIN throw a new
OrgException(OrgErrorCode.ORG_LAST_ADMIN_CANNOT_DEMOTE); add the new
OrgErrorCode.ORG_LAST_ADMIN_CANNOT_DEMOTE constant to your error code enum so
callers can surface a clear error for attempts to demote the last admin.

129-138: @Valid 추가 확인! 이전 리뷰에서 지적된 NPE 위험이 해결되었습니다. 🎉

Line 134에서 @RequestBody @Valid OrgRequest.UpdateRole dto로 잘 수정되었습니다. orgRole: null이 서비스 레이어까지 내려가던 위험이 차단되었네요.

한 가지 선택적 개선 사항이 있어요 — URL 경로 설계 불일치입니다.

현재 같은 "조직 멤버" 리소스를 다루는 두 엔드포인트의 경로가 서로 다릅니다:

메서드 현재 경로 (매핑 + base)
DELETE (Line 119) /api/org/{orgId}/members/{memberId}
PATCH (신규) /api/org/members/{orgId}/{memberId} ← 다름!

REST 컨벤션에서 동일 리소스는 동일한 경로 구조를 가져야 합니다. DELETE의 패턴(/{orgId}/members/{memberId})을 따르는 것이 더 일관적이고, Swagger 문서나 클라이언트 SDK 자동 생성 시에도 혼란이 없습니다.

♻️ URL 일관성 개선 제안
-    `@PatchMapping`("/members/{orgId}/{memberId}")
+    `@PatchMapping`("/{orgId}/members/{memberId}")
     public ResponseEntity<DataResponse<OrgResponse.OrgMemberDTO>> updateOrgMembersRole(

OrgControllerDocs.java@PathVariable 순서 및 @Operation 설명도 함께 업데이트하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java`
around lines 129 - 138, Change the PATCH mapping in OrgController so its URL
matches the DELETE pattern for the same resource: update the `@PatchMapping` on
updateOrgMembersRole to use "/{orgId}/members/{memberId}" (instead of
"/members/{orgId}/{memberId}"), keep the method signature
(updateOrgMembersRole(..., `@PathVariable` Long orgId, `@PathVariable` Long
memberId, `@RequestBody` `@Valid` OrgRequest.UpdateRole dto)) and then update the
corresponding docs class (OrgControllerDocs) to align `@PathVariable` order and
the `@Operation` description to reflect the corrected path; ensure no other
callers or tests expect the old path.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java`:
- Around line 43-48: The query findByUserIdAndOrgId causes N+1 because om.user
is LAZY; modify the repository query to JOIN FETCH the user (e.g., "SELECT om
FROM OrgMember om JOIN FETCH om.user u WHERE om.user.id = :userId AND
om.organization.id = :orgId AND u.status = :status") and change the method
signature to accept a UserStatus status parameter; then update all callers
(e.g., updateOrgMembersRole and removeMemberFromOrg) to pass UserStatus.ACTIVE
so OrgConverter.toOrgMemberDTO can access orgMember.getUser() without triggering
an extra select, matching the pattern used in findByOrganizationIdWithCursor.

---

Duplicate comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java`:
- Line 152: The docs interface OrgControllerDocs is missing `@Valid` on
`@RequestBody` parameters; add `@Valid` to the `@RequestBody` parameter for the
updateRole method (parameter type OrgRequest.UpdateRole) and likewise ensure the
createOrganization and modifyOrganization methods in OrgControllerDocs also
annotate their `@RequestBody` parameters with `@Valid` so the docs interface matches
the runtime controller signatures.
- Around line 143-144: Update the incorrect ApiResponse annotations in
OrgControllerDocs: replace the `@ApiResponse`(responseCode = "200", description =
"성공 (totalCount: 전체 멤버 수)") with a clear success description reflecting the
actual response payload for the endpoint, and change the mistaken
`@ApiResponse`(responseCode = "401", description = "잘못된 요청을 보낸 경우(ADMIN의 권한 변경)")
to the correct response code (e.g., responseCode = "400" or "403" as
appropriate) and update its description to accurately reflect the error
condition; modify the two `@ApiResponse` annotations shown (the
`@ApiResponse`(responseCode = "200", ...) and the `@ApiResponse`(responseCode =
"401", ...)) in OrgControllerDocs.java accordingly.

---

Nitpick comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/request/OrgRequest.java`:
- Around line 29-34: Add a single blank line after the closing brace of the
UpdateRole record so it is visually separated from the next Invite record; edit
around the UpdateRole and Invite record declarations (the UpdateRole record
closing "}" and the subsequent Invite record) to insert one empty line to
improve readability and match surrounding record spacing.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java`:
- Around line 129-138: The controller method updateOrgMembersRole calls
orgService.updateOrgMembersRole but the service currently blocks all
ADMIN→MEMBER demotions; change the service logic to allow demotion only when the
organization has at least 2 admins: in the service method check
orgMemberRepository.countByOrganizationAndRole(org, OrgRole.ADMIN) and if the
count <= 1 and the targetMember.getRole() == OrgRole.ADMIN throw a new
OrgException(OrgErrorCode.ORG_LAST_ADMIN_CANNOT_DEMOTE); add the new
OrgErrorCode.ORG_LAST_ADMIN_CANNOT_DEMOTE constant to your error code enum so
callers can surface a clear error for attempts to demote the last admin.
- Around line 129-138: Change the PATCH mapping in OrgController so its URL
matches the DELETE pattern for the same resource: update the `@PatchMapping` on
updateOrgMembersRole to use "/{orgId}/members/{memberId}" (instead of
"/members/{orgId}/{memberId}"), keep the method signature
(updateOrgMembersRole(..., `@PathVariable` Long orgId, `@PathVariable` Long
memberId, `@RequestBody` `@Valid` OrgRequest.UpdateRole dto)) and then update the
corresponding docs class (OrgControllerDocs) to align `@PathVariable` order and
the `@Operation` description to reflect the corrected path; ensure no other
callers or tests expect the old path.

Comment on lines +43 to +48
// userId 와 orgId 로 특정 OrgMember 조회
@Query("SELECT om FROM OrgMember om " +
"WHERE om.user.id = :userId " +
"AND om.organization.id = :orgId " +
"AND om.user.status = 'ACTIVE'")
Optional<OrgMember> findByUserIdAndOrgId(@Param("userId") Long userId, @Param("orgId") Long orgId);
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# 1. OrgMemberRepository 위치 및 내용 확인
echo "=== Finding OrgMemberRepository ==="
find . -name "OrgMemberRepository.java" -type f 2>/dev/null

echo ""
echo "=== OrgMemberRepository 전체 내용 ==="
find . -name "OrgMemberRepository.java" -type f -exec cat {} \;

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 2907


🏁 Script executed:

#!/bin/bash
# 2. OrgServiceImpl 확인 - updateOrgMembersRole 메서드
echo "=== Finding OrgServiceImpl ==="
find . -name "OrgServiceImpl.java" -type f 2>/dev/null

echo ""
echo "=== OrgServiceImpl 내용 ==="
find . -name "OrgServiceImpl.java" -type f -exec cat {} \;

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 12628


🏁 Script executed:

#!/bin/bash
# 3. OrgConverter 확인 - toOrgMemberDTO 메서드
echo "=== Finding OrgConverter ==="
find . -name "OrgConverter.java" -type f 2>/dev/null

echo ""
echo "=== OrgConverter 내용 ==="
find . -name "OrgConverter.java" -type f -exec cat {} \;

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 3746


🏁 Script executed:

#!/bin/bash
# 4. User entity 확인 - OrgMember와의 관계 설정 확인
echo "=== Finding User entity ==="
find . -name "User.java" -path "*/user/persistence/entity/*" -type f 2>/dev/null

echo ""
echo "=== User entity 내용 ==="
find . -name "User.java" -path "*/user/persistence/entity/*" -type f -exec head -100 {} \;

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1739


🏁 Script executed:

#!/bin/bash
# 5. OrgMember entity 확인 - User와의 FetchType 설정 확인
echo "=== Finding OrgMember entity ==="
find . -name "OrgMember.java" -type f 2>/dev/null

echo ""
echo "=== OrgMember entity 내용 ==="
find . -name "OrgMember.java" -type f -exec cat {} \;

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1318


🏁 Script executed:

#!/bin/bash
# 6. removeMemberFromOrg와 다른 메서드에서 findByUserIdAndOrgId 사용 패턴 확인
echo "=== Checking removeMemberFromOrg method usage ==="
rg -n -B 2 -A 8 "findByUserIdAndOrgId" src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java

echo ""
echo "=== UserStatus enum definition ==="
find . -name "UserStatus.java" -type f -exec cat {} \;

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1988


🏁 Script executed:

#!/bin/bash
# 7. updateOrgMembersRole 메서드 호출 이후 user 필드 접근 확인
echo "=== Detailed updateOrgMembersRole flow ==="
sed -n '203,226p' src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java

echo ""
echo "=== Check if OrgMemberDTO uses user fields ==="
find . -name "OrgResponse.java" -type f -exec grep -A 5 "class OrgMemberDTO" {} \;

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1152


🏁 Script executed:

#!/bin/bash
# 8. Find OrgResponse class with OrgMemberDTO record definition
echo "=== Finding OrgResponse ==="
find . -name "OrgResponse.java" -type f 2>/dev/null

echo ""
echo "=== OrgResponse class full content ==="
find . -name "OrgResponse.java" -type f -exec cat {} \;

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 2051


🏁 Script executed:

#!/bin/bash
# 9. Check all usages of findByUserIdAndOrgId to see if user fields are accessed
echo "=== All usages of findByUserIdAndOrgId ==="
rg -n "findByUserIdAndOrgId" src/main/java --type java -B 2 -A 10 | head -100

echo ""
echo "=== Check if user field is accessed in removeMemberFromOrg ==="
sed -n '170,197p' src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 9119


🏁 Script executed:

#!/bin/bash
# 10. Final check - see the complete return statement for updateOrgMembersRole
echo "=== Complete updateOrgMembersRole method (end) ==="
sed -n '226,230p' src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 230


🏁 Script executed:

#!/bin/bash
# Final verification - check how many times findByUserIdAndOrgId is used and in which contexts
echo "=== Summary of findByUserIdAndOrgId usage patterns ==="
rg -n "findByUserIdAndOrgId" src/main/java --type java | grep -v "Query\|Parameter\|Optional" | head -20

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 878


JOIN FETCH om.user 누락 — 실제 N+1 쿼리 발생

검증 결과, findByUserIdAndOrgId를 호출한 후 user 필드를 접근하는 메서드에서 실제로 N+1 쿼리가 발생합니다.

문제 시나리오: updateOrgMembersRole 메서드를 보면:

  1. 216번 줄에서 findByUserIdAndOrgId(memberId, orgId) 호출 → OrgMember 반환 (user는 LAZY)
  2. 227번 줄에서 OrgConverter.toOrgMemberDTO(orgMember) 호출
  3. OrgConverter 내부에서 orgMember.getUser().getName(), .getEmail(), .getProfileImageUrl() 접근
  4. 결과: 추가 SELECT 쿼리 1개 발생 (N+1)

클래스 레벨의 @Transactional이 있어서 LazyInitializationException은 피하지만, 불필요한 쿼리가 추가로 실행됩니다.

같은 레포지토리의 findByOrganizationIdWithCursor(Line 31-35)는 이미 JOIN FETCH m.user u를 명시하고 있으므로, 일관성 있게 이 메서드도 수정하는 것이 좋습니다.

추가로 om.user.status = 'ACTIVE'는 기술적으로 안전하지만(@Enumerated(EnumType.STRING) 적용됨), findByOrganizationIdWithCursor처럼 enum 파라미터로 바인딩하는 방식이 더 타입-세이프합니다.

♻️ 수정 제안
-@Query("SELECT om FROM OrgMember om " +
-        "WHERE om.user.id = :userId " +
-        "AND om.organization.id = :orgId " +
-        "AND om.user.status = 'ACTIVE'")
-Optional<OrgMember> findByUserIdAndOrgId(`@Param`("userId") Long userId, `@Param`("orgId") Long orgId);
+@Query("SELECT om FROM OrgMember om " +
+        "JOIN FETCH om.user u " +
+        "WHERE u.id = :userId " +
+        "AND om.organization.id = :orgId " +
+        "AND u.status = :status")
+Optional<OrgMember> findByUserIdAndOrgId(
+        `@Param`("userId") Long userId,
+        `@Param`("orgId") Long orgId,
+        `@Param`("status") UserStatus status
+);

호출부(updateOrgMembersRole, removeMemberFromOrg)에서 UserStatus.ACTIVE를 인자로 전달하도록 변경하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java`
around lines 43 - 48, The query findByUserIdAndOrgId causes N+1 because om.user
is LAZY; modify the repository query to JOIN FETCH the user (e.g., "SELECT om
FROM OrgMember om JOIN FETCH om.user u WHERE om.user.id = :userId AND
om.organization.id = :orgId AND u.status = :status") and change the method
signature to accept a UserStatus status parameter; then update all callers
(e.g., updateOrgMembersRole and removeMemberFromOrg) to pass UserStatus.ACTIVE
so OrgConverter.toOrgMemberDTO can access orgMember.getUser() without triggering
an extra select, matching the pattern used in findByOrganizationIdWithCursor.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 2

🧹 Nitpick comments (1)
src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java (1)

200-238: 동일 역할로의 변경 요청에 대한 처리가 없습니다.

현재 코드는 대상 멤버가 이미 요청된 역할을 갖고 있어도 (예: MEMBER → MEMBER) updateRole()을 호출하고 정상 응답을 반환합니다. 기능상 심각한 문제는 아니지만, 불필요한 dirty checking과 UPDATE 쿼리가 발생할 수 있습니다.

간단한 early return으로 이를 방지할 수 있습니다:

♻️ 제안
         OrgMember orgMember = orgMemberRepository.findByUserIdAndOrgId(memberId, orgId)
                 .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND));

+        // 동일 역할 변경 요청은 바로 반환
+        if (orgMember.getRole() == dto.orgRole()) {
+            return OrgConverter.toOrgMemberDTO(orgMember);
+        }
+
         // 4. ADMIN -> MEMBER 강등 시: 조직 내 ADMIN이 2명 이상이어야만 허용
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`
around lines 200 - 238, The method updateOrgMembersRole performs unnecessary
updates when the target member already has the requested role; add an early
return to skip work when orgMember.getRole() equals dto.orgRole(): before
performing the demotion check and before calling
orgMember.updateRole(dto.orgRole()), compare current role and if identical
return OrgConverter.toOrgMemberDTO(orgMember) immediately so no
dirty-checking/UPDATE occurs; ensure this check appears in updateOrgMembersRole
prior to the isDemoting logic that uses dto.orgRole() and orgMember.getRole().
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`:
- Around line 224-232: The demotion guard in OrgServiceImpl incorrectly uses
adminCount <= 2 which blocks demotion when there are exactly 2 admins; change
the check in the block that computes isDemoting (where orgMember.getRole() ==
OrgRole.ADMIN && dto.orgRole() == OrgRole.MEMBER) to use adminCount < 2 by
calling orgMemberRepository.countByOrganizationIdAndRole(orgId, OrgRole.ADMIN)
and throwing new OrgHandler(OrgErrorCode.ORG_LAST_ADMIN) only when adminCount <
2 so demotion is allowed when there are 2 admins but blocked when fewer than 2.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.java`:
- Around line 15-16: The enum constant ORG_LAST_ADMIN in OrgErrorCode has a
misleading message; update its message string to clearly explain the failure
reason (e.g., "마지막 ADMIN은 강등할 수 없습니다." or "조직에 최소 1명의 ADMIN이 필요하므로 강등할 수 없습니다.")
so API clients understand why the operation was rejected; locate the
ORG_LAST_ADMIN entry in OrgErrorCode and replace the current message "ADMIN은 2명
이상입니다." with the clearer failure-oriented message.

---

Duplicate comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`:
- Around line 208-210: The code in OrgServiceImpl retrieves Organization via
orgRepository.findById but does not guard against soft-deleted entities,
allowing actions on organizations with OrgStatus.DELETED; update the logic after
Organization organization = orgRepository.findById(orgId).orElseThrow(...) to
check organization.getStatus() (or organization.status) against
OrgStatus.DELETED and throw the same OrgHandler(OrgErrorCode.ORG_NOT_FOUND) (or
a suitable error) when deleted—follow the same pattern used in
getOrganizationDetail to ensure deleted orgs are treated as not found before
proceeding with role changes.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgMemberRepository.java`:
- Around line 44-49: The query in OrgMemberRepository.findByUserIdAndOrgId
should eagerly fetch the associated user to avoid N+1 when
OrgConverter.toOrgMemberDTO accesses
om.getUser().getName()/getEmail()/getProfileImageUrl(); change the JPQL to
include "JOIN FETCH om.user" consistent with findByOrganizationIdWithCursor and
replace the hard-coded "om.user.status = 'ACTIVE'" literal with a bound
parameter (e.g., :userStatus) so callers pass the active status constant; update
method signature to accept the status parameter accordingly.

---

Nitpick comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`:
- Around line 200-238: The method updateOrgMembersRole performs unnecessary
updates when the target member already has the requested role; add an early
return to skip work when orgMember.getRole() equals dto.orgRole(): before
performing the demotion check and before calling
orgMember.updateRole(dto.orgRole()), compare current role and if identical
return OrgConverter.toOrgMemberDTO(orgMember) immediately so no
dirty-checking/UPDATE occurs; ensure this check appears in updateOrgMembersRole
prior to the isDemoting logic that uses dto.orgRole() and orgMember.getRole().

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🧹 Nitpick comments (1)
src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java (1)

234-238: toOrgMemberDTO에서 User 엔티티 Lazy Loading이 발생할 수 있습니다.

OrgConverter.toOrgMemberDTO(orgMember)는 내부적으로 orgMember.getUser().getName(), getEmail(), getProfileImageUrl()을 호출합니다 (OrgConverter Line 77~81 참고). OrgMemberUser 필드가 FetchType.LAZY라면, 이 시점에 추가 SELECT 쿼리가 발생합니다.

현재는 단건 조회이므로 심각한 성능 이슈는 아니지만, findByUserIdAndOrgIdJOIN FETCH로 User까지 함께 가져오면 쿼리 하나를 절약할 수 있습니다.

#!/bin/bash
# Description: Check OrgMember entity's User field fetch type
echo "=== OrgMember entity - User relationship ==="
rg -n -B 2 -A 5 "User user" --type=java -g '*OrgMember.java'

echo ""
echo "=== findByUserIdAndOrgId query definition ==="
rg -n -B 3 -A 5 "findByUserIdAndOrgId" --type=java -g '*Repository*'
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`
around lines 234 - 238, OrgConverter.toOrgMemberDTO(orgMember) may trigger
lazy-loading of orgMember.getUser(), causing an extra SELECT; update the
retrieval so User is fetched with the OrgMember to avoid that. Modify the
repository method used by OrgServiceImpl (the findByUserIdAndOrgId query) to
include a JOIN FETCH for the User association (or add a new repository method
that does so) and use that repository call before calling
OrgConverter.toOrgMemberDTO(orgMember); alternatively, explicitly initialize
orgMember.getUser() (e.g., via repository join fetch or EntityManager) prior to
invoking OrgConverter.toOrgMemberDTO to prevent the additional lazy load.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`:
- Around line 203-206: The parameter name memberId is misleading (it is actually
a User ID) — update the parameter to a clear name (e.g., targetUserId) in
OrgServiceImpl methods (including the method that contains the self-check block
and removeMemberFromOrg), update any PathVariable/endpoint mappings to
/members/{orgId}/{userId} or to use {targetUserId}, and change all internal
usages (calls to findByUserIdAndOrgId(memberId, orgId) →
findByUserIdAndOrgId(targetUserId, orgId) and related lookups) so variable names
and API path match the actual semantics; ensure exception messages and logging
(OrgHandler/OrgErrorCode usage) reference the new parameter name for clarity.

---

Duplicate comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`:
- Around line 208-210: After retrieving Organization via orgRepository.findById
in OrgServiceImpl (the block using
orgRepository.findById(orgId).orElseThrow(...)), add the same soft-delete check
used in getOrganizationDetail: if organization.getStatus() == OrgStatus.DELETED
then throw new OrgHandler(OrgErrorCode.ORG_NOT_FOUND) (or the appropriate
OrgErrorCode used for deleted orgs) so soft-deleted organizations cannot be
acted on; place this check immediately after the organization assignment before
any member-role modification logic.

---

Nitpick comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`:
- Around line 234-238: OrgConverter.toOrgMemberDTO(orgMember) may trigger
lazy-loading of orgMember.getUser(), causing an extra SELECT; update the
retrieval so User is fetched with the OrgMember to avoid that. Modify the
repository method used by OrgServiceImpl (the findByUserIdAndOrgId query) to
include a JOIN FETCH for the User association (or add a new repository method
that does so) and use that repository call before calling
OrgConverter.toOrgMemberDTO(orgMember); alternatively, explicitly initialize
orgMember.getUser() (e.g., via repository join fetch or EntityManager) prior to
invoking OrgConverter.toOrgMemberDTO to prevent the additional lazy load.

Comment on lines +203 to +206
// 0. 본인 권한 변경 불가
if (Objects.equals(userId, memberId)) {
throw new OrgHandler(OrgErrorCode.ORG_CANNOT_ROLE_CHANGE_SELF);
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check the controller and repository to verify the memberId parameter semantics

echo "=== OrgController - Check for updateOrgMembersRole or similar endpoints ==="
rg -n -A 15 "updateOrgMembersRole|changeMemberRole|role.*member" --type=java | grep -v "ServiceImpl" | grep -v ".class" | head -50

echo ""
echo "=== OrgController - Look for PATCH endpoints ==="
rg -n -B 3 -A 15 "@PatchMapping|@PutMapping" --type=java | grep -A 15 "members.*memberId\|member.*role" | head -60

echo ""
echo "=== Check OrgRequest class for UpdateRole DTO ==="
rg -n -B 2 -A 5 "class UpdateRole|record UpdateRole" --type=java

echo ""
echo "=== Repository - Check findByUserIdAndOrgId signature ==="
rg -n -B 2 -A 3 "findByUserIdAndOrgId" --type=java

echo ""
echo "=== OrgServiceImpl - Check the full method including parameter and usage ==="
rg -n -B 5 -A 15 "updateOrgMembersRole|changeMemberRole" --type=java | grep -A 15 "ServiceImpl"

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 18113


memberId 파라미터명이 API 경로와 실제 동작이 일치하지 않습니다. 명확히 수정이 필요합니다.

API 경로 /members/{orgId}/{memberId}에서 memberIdOrgMember의 PK를 의미하는 것처럼 보이지만, 실제로는 User의 ID입니다.

증거:

  • Line 221의 findByUserIdAndOrgId(memberId, orgId) 에서 memberIduserId 파라미터 위치에 전달
  • Repository 메서드 시그니처: findByUserIdAndOrgId(@param("userId") Long userId, @param("orgId") Long orgId)
  • 실제로는 userId로 OrgMember를 검색하고 있음

문제점:

  1. 명명 불일치: API 클라이언트가 파라미터명을 보고 OrgMember PK를 보낼 가능성 높음 → "member not found" 오류 발생
  2. 일관성 부재: removeMemberFromOrg()도 동일한 패턴을 가지고 있음
  3. 유지보수성: 다른 개발자가 이 코드를 볼 때 의도를 파악하기 어려움

해결책:

  • 파라미터명을 memberIdtargetUserId 로 변경하거나
  • API 경로를 /members/{orgId}/{userId}로 변경
  • API 문서에 명시적으로 memberId가 User ID임을 기록

removeMemberFromOrg() 메서드도 동일하게 수정하세요.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java`
around lines 203 - 206, The parameter name memberId is misleading (it is
actually a User ID) — update the parameter to a clear name (e.g., targetUserId)
in OrgServiceImpl methods (including the method that contains the self-check
block and removeMemberFromOrg), update any PathVariable/endpoint mappings to
/members/{orgId}/{userId} or to use {targetUserId}, and change all internal
usages (calls to findByUserIdAndOrgId(memberId, orgId) →
findByUserIdAndOrgId(targetUserId, orgId) and related lookups) so variable names
and API path match the actual semantics; ensure exception messages and logging
(OrgHandler/OrgErrorCode usage) reference the new parameter name for clarity.

@kingmingyu kingmingyu merged commit f588e70 into develop Feb 21, 2026
2 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

✨ Feature 새로운 기능 추가

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 조직 맴버 관리 - 조직 맴버 권한 변경

3 participants