Skip to content
Open
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,6 +1,9 @@
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.NotBlank;
import jakarta.validation.constraints.NotNull;

public class OrgRequest {

Expand All @@ -22,4 +25,9 @@ public record Update (
String logoUrl
) {}

public record UpdateRole (
@Schema(description = "조직 내 역할(ADMIN / MEMBER)", example = "ADMIN", allowableValues = {"ADMIN", "MEMBER"})
@NotNull(message = "역할은 필수입니다.")
OrgRole orgRole
) {}
}
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 @@ -21,4 +21,7 @@ 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);
}
Original file line number Diff line number Diff line change
Expand Up @@ -190,4 +190,34 @@ public void removeMemberFromOrg(Long userId, Long orgId, Long memberId) {
// 5. 중간 테이블에서 해당 멤버 삭제
orgMemberRepository.delete(targetMember);
}

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

// 1. 조직 존재 여부 확인
Organization organization = orgRepository.findById(orgId)
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND));
Comment on lines +197 to +199
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. 해당 조직 내 멤버 조회 (상대방도 ADMIN이라면 MEMBER로 변경 불가)
OrgMember orgMember = orgMemberRepository.findByUserIdAndOrgId(memberId, orgId)
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND));

if (orgMember.getRole() == OrgRole.ADMIN) {
throw new OrgHandler(OrgErrorCode.ORG_CANNOT_ADMIN_TO_MEMBER);
}

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

// 변경된 멤버 정보를 DTO 로 반환
return OrgConverter.toOrgMemberDTO(orgMember);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ 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_ADMIN_TO_MEMBER(HttpStatus.BAD_REQUEST, "ORG_400_4", "ADMIN은 MEMBER로 변경할 수 없습니다."),

// 403
ORG_FORBIDDEN(HttpStatus.FORBIDDEN, "ORG_403_1", "해당 요청은 조직 생성자만 요청 가능합니다."),
Expand All @@ -24,7 +25,6 @@ public enum OrgErrorCode implements BaseErrorCode {
//409
ORG_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_409_1", "해당 조직은 이미 활성화 상태 입니다."),


//410
ORG_SOFT_DELETED(HttpStatus.GONE, "ORG_410_1", "해당 조직은 삭제된 조직입니다.(Soft Delete)"),
;
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
Expand Up @@ -17,14 +17,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 +40,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);

// 조직의 전체 멤버 수 조회
@Query("SELECT COUNT(m) FROM OrgMember m " +
"JOIN m.user u " +
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -125,4 +125,15 @@ public ResponseEntity<DataResponse<String>> removeMember(
orgService.removeMemberFromOrg(userId, orgId, memberId);
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 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 검사 적용되게 해주면 좋을 것 같아요!

) {
OrgResponse.OrgMemberDTO response = orgService.updateOrgMembersRole(userId, orgId, memberId, dto);
return ResponseEntity.ok(DataResponse.from(response));
}
Comment on lines +129 to +138
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🚨 @Valid 누락 — null orgRole이 입력되면 런타임 NPE가 발생합니다!

@RequestBody OrgRequest.UpdateRole dto@Valid가 없습니다. 다른 엔드포인트들을 보면:

  • @PostMapping("/create")@RequestBody @Valid OrgRequest.Create request
  • @PatchMapping("/{orgId}")@RequestBody @Valid OrgRequest.Update request

현재 엔드포인트는 @Valid가 없어서 OrgRequest.UpdateRole@NotNull(message = "역할은 필수입니다.") 검증이 전혀 실행되지 않습니다.

클라이언트가 {"orgRole": null} 또는 {} 를 전송하면 다음 흐름이 발생합니다:

  1. dto.orgRole()null
  2. orgMember.updateRole(null)this.role = null (메모리)
  3. OrgConverter.toOrgMemberDTO(orgMember)orgMember.getRole().name() 💥 NullPointerException
🐛 수정 방법
-    `@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/OrgController.java`
around lines 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.

}
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,21 @@ public ResponseEntity<DataResponse<String>> removeMember(
@PathVariable Long orgId,
@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).

);
}