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,7 +1,10 @@
package com.whereyouad.WhereYouAd.domains.organization.application.dto.request;

import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgRole;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;

public class OrgRequest {

Expand All @@ -23,6 +26,11 @@ public record Update (
String logoUrl
) {}

public record UpdateRole (
@Schema(description = "조직 내 역할(ADMIN / MEMBER)", example = "ADMIN", allowableValues = {"ADMIN", "MEMBER"})
@NotNull(message = "역할은 필수입니다.")
OrgRole orgRole
) {}
public record Invite(
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -66,19 +66,24 @@ public static Organization toOrganization(Long userId, OrgRequest.Create request
.build();
}

// 단일 OrgMember -> OrgMemberDTO 변환
public static OrgResponse.OrgMemberDTO toOrgMemberDTO(OrgMember orgMember) {
return new OrgResponse.OrgMemberDTO(
orgMember.getUser().getName(),
orgMember.getUser().getEmail(),
orgMember.getUser().getProfileImageUrl(),
orgMember.getRole().name()
);
}

// 조직 멤버 Slice DTO 변환 (무한 스크롤)
public static OrgResponse.OrgMemberSliceDTO toOrgMemberSliceDTO(
boolean hasNext,
String nextCursor,
List<OrgMember> orgMembers
) {
List<OrgResponse.OrgMemberDTO> memberDTOs = orgMembers.stream()
.map(m -> new OrgResponse.OrgMemberDTO(
m.getUser().getName(),
m.getUser().getEmail(),
m.getUser().getProfileImageUrl(),
m.getRole().name()
))
.map(OrgConverter::toOrgMemberDTO)
.toList();

return new OrgResponse.OrgMemberSliceDTO(
Expand All @@ -87,4 +92,4 @@ public static OrgResponse.OrgMemberSliceDTO toOrgMemberSliceDTO(
memberDTOs
);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ public interface OrgService {
// orgId 조직에서 memberId에 해당하는 맴버 제거
void removeMemberFromOrg(Long userId, Long orgId, Long memberId);

// 조직 내 멤버 권한 변경 메서드
OrgResponse.OrgMemberDTO updateOrgMembersRole(Long userId, Long orgId, Long memberId, OrgRequest.UpdateRole dto);

OrgResponse.OrgInvitationResponse sendOrgInvitation(Long userId, Long orgId, String email);

OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String token);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -197,6 +197,47 @@ public void removeMemberFromOrg(Long userId, Long orgId, Long memberId) {
orgMemberRepository.delete(targetMember);
}

public OrgResponse.OrgMemberDTO updateOrgMembersRole(Long userId, Long orgId, Long memberId,
OrgRequest.UpdateRole dto) {

// 0. 본인 권한 변경 불가
if (Objects.equals(userId, memberId)) {
throw new OrgHandler(OrgErrorCode.ORG_CANNOT_ROLE_CHANGE_SELF);
}
Comment on lines +203 to +206
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.


// 1. 조직 존재 여부 확인
Organization organization = orgRepository.findById(orgId)
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND));
Comment on lines +208 to +210
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.


// 2. 요청자가 해당 조직의 ADMIN인지 확인
OrgMember requester = orgMemberRepository.findByUserIdAndOrgId(userId, orgId)
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND));

if (requester.getRole() != OrgRole.ADMIN) {
throw new OrgHandler(OrgErrorCode.ORG_MEMBER_FORBIDDEN);
}

// 3. 권한 변경 대상 멤버 조회
OrgMember orgMember = orgMemberRepository.findByUserIdAndOrgId(memberId, orgId)
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND));

// 4. ADMIN -> MEMBER 강등 시: 조직 내 ADMIN이 2명 이상이어야만 허용
// 해당 멤버 ADMIN, 요청 역할 MEMBER인 경우
boolean isDemoting = orgMember.getRole() == OrgRole.ADMIN && dto.orgRole() == OrgRole.MEMBER;
if (isDemoting) {
long adminCount = orgMemberRepository.countByOrganizationIdAndRole(orgId, OrgRole.ADMIN);
if (adminCount < 2) {
throw new OrgHandler(OrgErrorCode.ORG_LAST_ADMIN);
}
}

// 역할 변경 (더티체킹)
orgMember.updateRole(dto.orgRole());

// 변경된 멤버 정보를 DTO 로 반환
return OrgConverter.toOrgMemberDTO(orgMember);
}

@Override
// 조직 초대 이메일 보내기
public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long userId, Long orgId, String email) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,29 +12,25 @@ public enum OrgErrorCode implements BaseErrorCode {
ORG_NAME_DUPLICATE(HttpStatus.BAD_REQUEST, "ORG_400_1", "사용자가 이미 속해있는 조직의 이름입니다."),
ORG_CANNOT_KICK_SELF(HttpStatus.BAD_REQUEST, "ORG_400_2", "자기 자신을 추방할 수 없습니다."),
ORG_CANNOT_KICK_ADMIN(HttpStatus.BAD_REQUEST, "ORG_400_3", "ADMIN은 추방할 수 없습니다."),
ORG_CANNOT_ROLE_CHANGE_SELF(HttpStatus.BAD_REQUEST, "ORG_400_4", "본인의 역할은 변경할 수 없습니다."),
ORG_LAST_ADMIN(HttpStatus.BAD_REQUEST, "ORG_400_5", "마지막 ADMIN은 강등할 수 없습니다."),

// 403
ORG_FORBIDDEN(HttpStatus.FORBIDDEN, "ORG_403_1", "해당 요청은 조직 생성자만 요청 가능합니다."),
ORG_MEMBER_FORBIDDEN(HttpStatus.FORBIDDEN, "ORG_403_2", "해당 요청은 ADMIN 권한을 가진 멤버만 요청 가능합니다."),
ORG_INVITATION_FORBIDDEN_USER(HttpStatus.FORBIDDEN, "ORG_INVITATION_403_1", "초대된 이메일과 현재 로그인한 사용자의 이메일이 일치하지 않습니다."),

// 404
ORG_NOT_FOUND(HttpStatus.NOT_FOUND, "ORG_404_1", "해당 id 의 조직이 존재하지 않습니다."),
ORG_MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "ORG_404_2", "해당 멤버가 조직에 존재하지 않습니다."),

//409
ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다."),
ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."),

//410
ORG_SOFT_DELETED(HttpStatus.GONE, "ORG_410_1", "해당 조직은 삭제된 조직입니다.(Soft Delete)"),

// 409
ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."),

// 400
ORG_INVITATION_INVALID(HttpStatus.BAD_REQUEST, "ORG_INVITATION_400", "조직 초대 토큰이 만료되었거나 유효하지 않습니다."),

// 403
ORG_INVITATION_FORBIDDEN_USER(HttpStatus.FORBIDDEN, "ORG_INVITATION_403_1", "초대된 이메일과 현재 로그인한 사용자의 이메일이 일치하지 않습니다."),
;

private final HttpStatus httpStatus;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,8 @@ public class OrgMember { //중간 테이블이므로 BaseEntity 미적용
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "org_id")
private Organization organization;

public void updateRole(OrgRole role) {
this.role = role;
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.whereyouad.WhereYouAd.domains.organization.persistence.repository;

import com.whereyouad.WhereYouAd.domains.organization.domain.constant.OrgRole;
import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.OrgMember;
import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization;
import com.whereyouad.WhereYouAd.domains.user.domain.constant.UserStatus;
Expand All @@ -17,14 +18,7 @@ public interface OrgMemberRepository extends JpaRepository<OrgMember, Long> {

//User 가 가진 OrgMember 모두 추출하는 메서드
List<OrgMember> findOrgMemberByUser(User user);

// 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);


//userId 를 통해 OrgMember 추출 -> Organization 의 status 가 ACTIVE 인 경우에만 조회
@Query(value = "select om from OrgMember om join fetch om.organization o where om.user.id = :userId and o.status = 'ACTIVE'")
List<OrgMember> findOrgMemberByUserId(@Param("userId") Long userId);
Expand All @@ -47,6 +41,13 @@ Slice<OrgMember> findByOrganizationIdWithCursor(
Pageable pageable
);

// 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);
Comment on lines +44 to +49
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.


// 조직의 전체 멤버 수 조회
@Query("SELECT COUNT(m) FROM OrgMember m " +
"JOIN m.user u " +
Expand All @@ -58,4 +59,7 @@ int countByOrganizationIdAndUserStatus(
);

Boolean existsByUserAndOrganization(User user, Organization organization);

// 조직 id에 해당하는 역할 인원 수 조회
long countByOrganizationIdAndRole(Long orgId, OrgRole orgRole);
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,6 +53,7 @@ public ResponseEntity<DataResponse<OrgResponse.OrgDetail>> getOrganizationDetail
);
}


@PatchMapping("/{orgId}")
public ResponseEntity<DataResponse<OrgResponse.Update>> modifyOrganization(
@AuthenticationPrincipal(expression = "userId") Long userId,
Expand Down Expand Up @@ -125,6 +126,17 @@ public ResponseEntity<DataResponse<String>> removeMember(
return ResponseEntity.ok(DataResponse.from("해당 맴버가 조직에서 제외되었습니다."));
}

@PatchMapping("/members/{orgId}/{memberId}")
public ResponseEntity<DataResponse<OrgResponse.OrgMemberDTO>> updateOrgMembersRole(
@AuthenticationPrincipal(expression = "userId") Long userId,
@PathVariable Long orgId,
@PathVariable Long memberId,
@RequestBody @Valid OrgRequest.UpdateRole dto
) {
OrgResponse.OrgMemberDTO response = orgService.updateOrgMembersRole(userId, orgId, memberId, dto);
return ResponseEntity.ok(DataResponse.from(response));
}

@PostMapping("/members/{orgId}/invitation")
public ResponseEntity<DataResponse<OrgResponse.OrgInvitationResponse>> sendOrgInvitation(
@AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable Long orgId,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,6 +135,23 @@ public ResponseEntity<DataResponse<String>> removeMember(
@PathVariable Long memberId
);

@Operation(
summary = "조직 맴버 권한 변경 API",
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.

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

@ApiResponse(responseCode = "403", description = "권한이 부족한 경우(요청을 보낸 유저의 권한이 ADMIN이 아닌 경우)"),
@ApiResponse(responseCode = "404", description = "해당 id의 데이터 존재 X")
})
ResponseEntity<DataResponse<OrgResponse.OrgMemberDTO>> updateOrgMembersRole(
@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).

);

@Operation(summary = "조직 초대 이메일 발송 API", description = "조직 관리자가 이메일을 입력하여 새로운 멤버를 초대합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
Expand Down