Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
dd971ed
:sparkles: feat: 조직 맴버 초대 - 이메일 전송 로직 구현
jinnieusLab Feb 17, 2026
99215bb
:recycle: refactor: 조직 맴버 초대 uuid로 변경
jinnieusLab Feb 18, 2026
8e49ab3
:sparkles: feat: 조직 멤버 초대 수락 구현
jinnieusLab Feb 18, 2026
8d0eb8d
:wrench: chore: 조직 초대 관련 Redis key명 변경
jinnieusLab Feb 18, 2026
2835466
:wrench: chore: Redis 사용 토큰 삭제 추가
jinnieusLab Feb 18, 2026
61a343c
:recycle: refactor: 조직 초대 에러 처리
jinnieusLab Feb 18, 2026
f2008fb
:wrench: chore: Redis value 값 분리 수정
jinnieusLab Feb 18, 2026
de41291
:wrench: chore: 초대 링크 로컬과 배포 서버 구분
jinnieusLab Feb 18, 2026
2c48429
:recycle: refactor: 조직 초대 요청 형식 추가
jinnieusLab Feb 18, 2026
efc79ee
:recycle: refactor: 초대 수락 유저 검증 추가
jinnieusLab Feb 18, 2026
08144a9
:memo: docs: 조직 초대 Swagger 명세 추가
jinnieusLab Feb 18, 2026
7a50428
:recycle: refactor: 초대 수락 유저 인가 에러 코드 추가
jinnieusLab Feb 18, 2026
0a26605
:wrench: chore: develop 브랜치 merge 및 conflicts 해결
jinnieusLab Feb 18, 2026
1892a75
:recycle: refactor: 초대자 권한 검증 추가
jinnieusLab Feb 18, 2026
77408be
:recycle: refactor: 초대 수락 API Post로 변경
jinnieusLab Feb 18, 2026
cad16a9
:recycle: refactor: 조직 초대 요청 이메일 형식 검증 추가
jinnieusLab Feb 18, 2026
474ad2d
:wrench: chore: 조직 초대 API 관련 수정
jinnieusLab Feb 18, 2026
9e5bdf9
:recycle: refactor: 조직 초대 수락 시 권한 MEMBER로 변경
jinnieusLab Feb 19, 2026
1a66823
:twisted_rightwards_arrows: chore: develop 브랜치 merge 및 conflicts 해결
jinnieusLab Feb 19, 2026
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,5 +1,6 @@
package com.whereyouad.WhereYouAd.domains.organization.application.dto.request;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;

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

public record Invite(
@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
String email
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -68,4 +68,10 @@ public record OrgMemberDTO(
String profileImageUrl,
String role
) {}

public record OrgInvitationResponse(
Long orgId,
String message,
String email
) {}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,25 @@
import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization;
import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;

import java.time.LocalDateTime;

public class OrgMemberConverter {

public static OrgMember toOrgMemberADMIN(User user, Organization organization) {
return OrgMember.builder()
.user(user)
.organization(organization)
.joinedAt(organization.getCreatedAt()) //생성한 사람의 조직 합류 시간은 조직 생성 시간과 동일
.role(OrgRole.ADMIN) //생성한 사람은 ADMIN
.joinedAt(organization.getCreatedAt()) // 생성한 사람의 조직 합류 시간은 조직 생성 시간과 동일
.role(OrgRole.ADMIN) // 생성한 사람은 ADMIN
.build();
}

public static OrgMember toOrgMemberMEMBER(User user, Organization organization) {
return OrgMember.builder()
.user(user)
.organization(organization)
.joinedAt(LocalDateTime.now()) // 조직 초대 완료 시점
.role(OrgRole.MEMBER) // 초대된 사람은 MEMBER (default)
.build();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,8 @@ public interface OrgService {

// orgId 조직에서 memberId에 해당하는 맴버 제거
void removeMemberFromOrg(Long userId, Long orgId, Long memberId);

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

OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String token);
}
Original file line number Diff line number Diff line change
Expand Up @@ -12,16 +12,19 @@
import com.whereyouad.WhereYouAd.domains.organization.persistence.entity.Organization;
import com.whereyouad.WhereYouAd.domains.organization.persistence.repository.OrgMemberRepository;
import com.whereyouad.WhereYouAd.domains.organization.persistence.repository.OrgRepository;
import com.whereyouad.WhereYouAd.domains.user.domain.service.EmailService;
import com.whereyouad.WhereYouAd.domains.user.exception.code.UserErrorCode;
import com.whereyouad.WhereYouAd.domains.user.exception.handler.UserHandler;
import com.whereyouad.WhereYouAd.domains.user.persistence.entity.User;
import com.whereyouad.WhereYouAd.domains.user.persistence.repository.UserRepository;
import com.whereyouad.WhereYouAd.global.utils.RedisUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.Objects;
import java.util.UUID;

@Service
@Transactional
Expand All @@ -32,6 +35,9 @@ public class OrgServiceImpl implements OrgService {
private final OrgMemberRepository orgMemberRepository;
private final UserRepository userRepository;

private final RedisUtil redisUtil;
private final EmailService emailService;

// 조직(워크스페이스) 생성 메서드
public OrgResponse.Create createOrganization(Long userId, OrgRequest.Create request) {

Expand Down Expand Up @@ -190,4 +196,77 @@ public void removeMemberFromOrg(Long userId, Long orgId, Long memberId) {
// 5. 중간 테이블에서 해당 멤버 삭제
orgMemberRepository.delete(targetMember);
}

@Override
// 조직 초대 이메일 보내기
public OrgResponse.OrgInvitationResponse sendOrgInvitation(Long userId, Long orgId, String email) {
Organization organization = orgRepository.findById(orgId)
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND));

// 초대자와 조직 관계 검증 (초대자가 조직의 멤버인지 확인)
User sender = userRepository.findById(userId)
.orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND));

if (!orgMemberRepository.existsByUserAndOrganization(sender, organization)) {
// 초대자가 조직 멤버가 아님 -> 권한 없음
throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN);
}
Comment on lines +206 to +213
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

🧩 Analysis chain

🏁 Script executed:

# OrgControllerDocs에서 sendOrgInvitation 관련 설명 찾기
find . -type f -name "*ControllerDocs*.java" | head -20

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 472


🏁 Script executed:

# OrgServiceImpl 파일 위치 및 내용 확인
find . -type f -name "OrgServiceImpl.java" -exec wc -l {} \;

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 174


🏁 Script executed:

# sendOrgInvitation 메서드 전체 구현 찾기
rg -n "sendOrgInvitation" --type=java -C 15

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 16434


🏁 Script executed:

# OrgRole enum 구조 확인
find . -type f -name "OrgRole.java" -o -name "OrgRole*.java"

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 164


🏁 Script executed:

# orgMemberRepository 메서드 확인
rg -n "orgMemberRepository|OrgMemberRepository" --type=java -A 3

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 11013


🏁 Script executed:

cat src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/constant/OrgRole.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 187


🏁 Script executed:

find . -type f -name "OrgErrorCode.java" -exec cat {} \;

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1694


🏁 Script executed:

# sendOrgInvitation 메서드 전체 흐름 확인
sed -n '202,230p' src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 1451


Swagger 문서와 초대 권한 검증이 불일치합니다.

OrgControllerDocs에서는 "조직 관리자가 이메일을 입력하여 새로운 멤버를 초대합니다"라고 명시했는데, 실제 코드는 existsByUserAndOrganization만 검사하여 일반 MEMBER도 초대를 보낼 수 있습니다.

의도적으로 모든 멤버가 초대할 수 있다면 Swagger 설명을 수정하고, ADMIN만 허용해야 한다면 역할 검증 로직을 추가하세요.

ADMIN만 초대 가능하도록 수정하는 경우 예시
         if (!orgMemberRepository.existsByUserAndOrganization(sender, organization)) {
             throw new OrgHandler(OrgErrorCode.ORG_FORBIDDEN);
         }

+        // ADMIN만 초대 가능하도록 역할 검증
+        OrgMember senderMember = orgMemberRepository.findByUserIdAndOrgId(userId, orgId)
+                .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND));
+        if (senderMember.getRole() != OrgRole.ADMIN) {
+            throw new OrgHandler(OrgErrorCode.ORG_MEMBER_FORBIDDEN);
+        }
🤖 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 206 - 213, The current check in OrgServiceImpl only verifies
membership via orgMemberRepository.existsByUserAndOrganization (allowing any
MEMBER to invite) which mismatches the OrgControllerDocs; update the
invite-authority check to require the inviter be an ADMIN: after loading User
sender (userRepository.findById) retrieve the OrgMember (e.g.,
orgMemberRepository.findByUserAndOrganization or equivalent) for the sender and
assert its role/authority equals ADMIN, throwing
OrgHandler(OrgErrorCode.ORG_FORBIDDEN) if not; ensure you remove or replace the
simple existsByUserAndOrganization check and use the OrgMember role field for
the authorization decision so only ADMINs can invite.


// 초대 완료 여부 확인, 가입 여부에 상관 없이 이메일 발송
userRepository.findUserByEmail(email).ifPresent(user -> {
if (orgMemberRepository.existsByUserAndOrganization(user, organization)) {
throw new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE);
}
});

// Redis key = 임의의 UUID 토큰(조직 초대 이메일 내 링크를 구별)
String token = UUID.randomUUID().toString();
// Redis value = 조직 아이디와 이메일의 조합
String value = orgId + ":" + email;
redisUtil.setDataExpire("INVITE:" + token, value, 3600 * 24L);

emailService.sendEmailForOrgInvitation(token, email, organization.getName());

return new OrgResponse.OrgInvitationResponse(orgId, "조직 멤버 초대 이메일을 전송하였습니다.", email);
}

@Override
// 조직 초대 수락 (이메일 내 링크 클릭 시)
public OrgResponse.OrgInvitationResponse acceptOrgInvitation(Long userId, String token) {
// 링크 만료 또는 유효하지 않을 시
if (token == null)
throw new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID);

// Redis 내 UUID(key)에 대한 email(value) 비교
String value = redisUtil.getData("INVITE:" + token);
if (value == null) {
throw new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID);
}

String[] valueForSplit = value.split(":");
String email = valueForSplit[1];
Comment on lines +246 to +247
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
# First, find the file and check its structure
find . -type f -name "OrgServiceImpl.java" | head -5

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 170


🏁 Script executed:

#!/bin/bash
# Once we have the file path, let's examine the relevant sections
# Find lines 200-270 to see the full context of both methods
if [ -f "src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java" ]; then
  sed -n '200,270p' "src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java"
else
  find . -name "OrgServiceImpl.java" -exec sed -n '200,270p' {} \;
fi

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 3050


🏁 Script executed:

#!/bin/bash
# Let's also check where the Redis value is being set (the "orgId:email" format)
if [ -f "src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java" ]; then
  grep -n "redisTemplate\|setValue\|set(" "src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java" | head -20
fi

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 55


🏁 Script executed:

#!/bin/bash
# Search for Redis operations and INVITE token handling
rg -n "INVITE|redis|set\(" --type=java "src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java"

Repository: WhereYouAd/WhereYouAd-Backend

Length of output: 307


⚠️ Redis 값 파싱 시 split(":")의 한계 — 안전하지 않은 분할

Redis에 "orgId:email" 형태로 저장하되, split(":")로 분리하는 것은 이메일에 콜론이 포함될 경우 문제가 됩니다. RFC 5321 표준상 이메일 로컬 부분은 따옴표 처리 시 특수문자(: 포함) 사용이 가능하며, 이 경우 파싱이 깨집니다.

예: "user:name"@example.com 형태의 이메일이 Redis에 123:user:name@example.com으로 저장되면, split(":")[1]user만 추출되어 나머지 :name@example.com을 잃습니다.

더 근본적으로, Redis 값 포맷에 콜론이 추가되거나 변경되는 순간 이 파싱 로직은 깨질 수 있습니다. split(":", 2)를 사용하면 첫 번째 콜론만 기준으로 분리되어 안전합니다.

✅ 수정 제안
-        String[] valueForSplit = value.split(":");
+        String[] valueForSplit = value.split(":", 2);
         String email = valueForSplit[1];
📝 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
String[] valueForSplit = value.split(":");
String email = valueForSplit[1];
String[] valueForSplit = value.split(":", 2);
String email = valueForSplit[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 246 - 247, In OrgServiceImpl where you parse the Redis string into
parts using value.split(":"), the current logic (String[] valueForSplit =
value.split(":"); String email = valueForSplit[1];) will break if the email
contains colons; change the split to limit to two parts (e.g., use
value.split(":", 2)) so only the first colon is used as the delimiter and then
derive email from the second element, ensuring valueForSplit and email
extraction remain safe even when the email contains colons.


// 로그인한 사용자 검증
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserHandler(UserErrorCode.USER_NOT_FOUND));

// 초대된 이메일과 현재 로그인한 사용자의 이메일이 일치하는지 확인
if (!user.getEmail().equals(email)) {
throw new OrgHandler(OrgErrorCode.ORG_INVITATION_FORBIDDEN_USER);
}

Organization organization = orgRepository.findById(Long.parseLong(valueForSplit[0]))
.orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND));

// 이미 멤버인지 중복 체크
if (orgMemberRepository.existsByUserAndOrganization(user, organization))
throw new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE);

orgMemberRepository.save(OrgMemberConverter.toOrgMemberMEMBER(user, organization));

// Redis 사용 토큰 삭제
redisUtil.deleteData("INVITE:" + token);
Comment on lines +258 to +268
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

토큰 수락 시 TOCTOU(Time-of-Check to Time-of-Use) 경합 조건이 있습니다.

현재 흐름:

  1. Line 262: 멤버 중복 체크 ✅
  2. Line 265: OrgMember 저장
  3. Line 268: Redis 토큰 삭제

문제는 두 사용자가 동시에 같은 토큰으로 수락 요청을 보내면, 둘 다 중복 체크를 통과하고 멤버가 두 번 생성될 수 있다는 것입니다. Redis 토큰 삭제가 DB 저장 이후에 일어나기 때문에, 그 사이 시간 동안 다른 요청이 동일 토큰을 사용할 수 있어요.

해결 방법: Redis 토큰 삭제를 먼저 수행하고(일종의 원자적 잠금 역할), 삭제 성공 시에만 멤버를 등록하세요. 이렇게 하면 토큰을 먼저 소비한 요청만 진행됩니다.

🛡️ 수정 제안
+        // 토큰을 먼저 소비하여 동시 수락 방지 (원자적 삭제)
+        String redisKey = "INVITE:" + token;
+        redisUtil.deleteData(redisKey);

         // 이미 멤버인지 중복 체크
         if (orgMemberRepository.existsByUserAndOrganization(user, organization))
             throw new OrgHandler(OrgErrorCode.ORG_MEMBER_ALREADY_ACTIVE);

         orgMemberRepository.save(OrgMemberConverter.toOrgMemberMEMBER(user, organization));

-        // Redis 사용 토큰 삭제
-        redisUtil.deleteData("INVITE:" + token);
🤖 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 258 - 268, Move the Redis token consumption ahead of the DB write
to prevent TOCTOU races: call redisUtil.deleteData("INVITE:"+token) first and
verify it actually deleted the key (use the method's boolean/response to check
success), if deletion failed throw the appropriate OrgHandler (e.g., token
invalid/consumed) and abort; only after successful deletion, load the
Organization via orgRepository.findById(...), perform the duplicate check with
orgMemberRepository.existsByUserAndOrganization(user, organization) and then
call orgMemberRepository.save(OrgMemberConverter.toOrgMemberMEMBER(user,
organization)); also ensure the enclosing method is transactional so the member
save is atomic relative to other DB ops.


return new OrgResponse.OrgInvitationResponse(organization.getId(), "조직 멤버 초대 이메일을 수락하였습니다.", email);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,17 @@ 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)"),

// 409
ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."),
Comment on lines +30 to +31
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

ORG_MEMBER_ALREADY_ACTIVE 메시지가 오해를 줄 수 있습니다.

이 에러는 초대를 수락하려 할 때 이미 조직의 멤버인 경우에 발생합니다. 그런데 메시지 "이미 해당 조직에 초대되어있습니다"는 '초대는 받았지만 아직 수락 전인 대기 상태'처럼 읽힐 수 있어 혼란스럽습니다. 실제 상황을 더 정확하게 전달하는 메시지로 바꾸는 게 좋습니다.

✏️ 제안하는 수정
- ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_1", "이미 해당 조직에 초대되어있습니다."),
+ ORG_MEMBER_ALREADY_ACTIVE(HttpStatus.CONFLICT, "ORG_MEMBER_409_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/exception/code/OrgErrorCode.java`
around lines 23 - 24, OrgErrorCode 열거형의 ORG_MEMBER_ALREADY_ACTIVE 항목 메시지가 "이미 해당
조직에 초대되어있습니다."로 되어 있어 오해의 소지가 있으니 이 enum 상수(ORG_MEMBER_ALREADY_ACTIVE)의 설명 문자열을
조직에 이미 멤버임을 명확히 나타내는 문구로 변경하세요; 예를 들어 OrgErrorCode의 ORG_MEMBER_ALREADY_ACTIVE 값을
찾아 기존 메시지를 "이미 조직의 멤버입니다." 또는 동등한 명확한 한국어 문구로 교체하면 됩니다.


// 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 @@ -56,4 +56,6 @@ int countByOrganizationIdAndUserStatus(
@Param("orgId") Long orgId,
@Param("status") UserStatus status
);

Boolean existsByUserAndOrganization(User user, Organization organization);
}
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,6 @@ public ResponseEntity<DataResponse<OrgResponse.OrgDetail>> getOrganizationDetail
);
}


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

@PostMapping("/members/{orgId}/invitation")
public ResponseEntity<DataResponse<OrgResponse.OrgInvitationResponse>> sendOrgInvitation(
@AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable Long orgId,
@RequestBody @Valid OrgRequest.Invite request) {
OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.sendOrgInvitation(userId, orgId,
request.email());
return ResponseEntity.ok(DataResponse.from(orgInvitationResponse));
}

@PostMapping("/invitations/{token}")
public ResponseEntity<DataResponse<OrgResponse.OrgInvitationResponse>> acceptOrgInvitation(
@AuthenticationPrincipal(expression = "userId") Long userId, @PathVariable String token) {
OrgResponse.OrgInvitationResponse orgInvitationResponse = orgService.acceptOrgInvitation(userId, token);
return ResponseEntity.ok(DataResponse.from(orgInvitationResponse));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -134,4 +134,30 @@ public ResponseEntity<DataResponse<String>> removeMember(
@PathVariable Long orgId,
@PathVariable Long memberId
);

@Operation(summary = "조직 초대 이메일 발송 API", description = "조직 관리자가 이메일을 입력하여 새로운 멤버를 초대합니다.")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "401", description = "로그인 필요"),
@ApiResponse(responseCode = "403", description = "조직 멤버가 아닌 사용자의 요청"),
@ApiResponse(responseCode = "404", description = "조직을 찾을 수 없음"),
@ApiResponse(responseCode = "409", description = "이미 조직에 가입된 사용자")
})
public ResponseEntity<DataResponse<OrgResponse.OrgInvitationResponse>> sendOrgInvitation(
@AuthenticationPrincipal(expression = "userId") Long userId,
@PathVariable Long orgId,
@RequestBody @Valid OrgRequest.Invite request
);

@Operation(summary = "조직 초대 수락 API", description = "이메일로 받은 초대 토큰을 통해 조직 가입을 수락합니다. (로그인 필수, 본인 확인)")
@ApiResponses({
@ApiResponse(responseCode = "200", description = "성공"),
@ApiResponse(responseCode = "400", description = "유효하지 않거나 만료된 토큰"),
@ApiResponse(responseCode = "401", description = "로그인 필요"),
@ApiResponse(responseCode = "403", description = "초대된 이메일과 로그인한 사용자가 불일치")
})
public ResponseEntity<DataResponse<OrgResponse.OrgInvitationResponse>> acceptOrgInvitation(
@AuthenticationPrincipal(expression = "userId") Long userId,
@PathVariable String token
);
Comment on lines +152 to +162
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

acceptOrgInvitation에 409 응답 코드가 빠져 있습니다.

OrgServiceImpl.acceptOrgInvitation에서 이미 조직 멤버인 경우 ORG_MEMBER_ALREADY_ACTIVE (409 Conflict)를 던지는데, Swagger 문서에는 이 응답이 누락되어 있습니다. API 사용자(프론트엔드 개발자)가 이 케이스를 처리하지 못할 수 있어요.

✏️ 수정 제안
     `@ApiResponses`({
             `@ApiResponse`(responseCode = "200", description = "성공"),
             `@ApiResponse`(responseCode = "400", description = "유효하지 않거나 만료된 토큰"),
             `@ApiResponse`(responseCode = "401", description = "로그인 필요"),
-            `@ApiResponse`(responseCode = "403", description = "초대된 이메일과 로그인한 사용자가 불일치")
+            `@ApiResponse`(responseCode = "403", description = "초대된 이메일과 로그인한 사용자가 불일치"),
+            `@ApiResponse`(responseCode = "409", 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`
around lines 152 - 162, Update the Swagger docs for acceptOrgInvitation to
include the 409 response for already-active members: add an
`@ApiResponse`(responseCode = "409", description = "이미 조직
멤버(ORG_MEMBER_ALREADY_ACTIVE)") to the method declaration so the API contract
matches OrgServiceImpl.acceptOrgInvitation throwing ORG_MEMBER_ALREADY_ACTIVE;
ensure the description clearly states this is the conflict when the user is
already a member.

}
Loading