Skip to content

Feat/#107 조직 맴버 조회 - 초대 중인 맴버 조회 API#108

Merged
kingmingyu merged 11 commits intodevelopfrom
feat/#107
Mar 31, 2026
Merged

Feat/#107 조직 맴버 조회 - 초대 중인 맴버 조회 API#108
kingmingyu merged 11 commits intodevelopfrom
feat/#107

Conversation

@kingmingyu
Copy link
Copy Markdown
Collaborator

@kingmingyu kingmingyu commented Mar 30, 2026

📌 관련 이슈

🚀 개요

이번 PR에서 변경된 핵심 내용을 요약해주세요.
초대 대기 중인 맴버 조회 API 추가

📄 작업 내용

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

  • 초대 대기 중인 맴버를 조회하기 위해 DB에 저장(OrgInvitation 추가, 기존 erdCloud에 존재하던 엔티티)
    +필요한 속성로 간소화했습니다.
image
  • 현재 DB에 저장된 만료 시간 이전의 맴버 조회 로직 및 API 추가
  • 매 02:00 마다 만료 시간이 지난 초대장은 DB에서 삭제(-> status 속성 불필요)
  • 기존 조직 맴버 이메일로 초대 및 수락 시 DB 저장 및 삭제 로직 추

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

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

  • 요청 값
image
  • 응답 값
image

✅ 체크리스트

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

🔍 리뷰 포인트 (Review Points)

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

  • 기존 orgMember 테이블에 status 속성 추가하고 기존 맴버 조회 API에 status 필드를 추가로 받는 식으로 진행하려고 했으나... 이렇게 하면 기존 검증 로직에 대대적인 수정이 필요할 것 같아 마침 ERD에 있는 OrgInvitation 엔티티를 새로 만들었습니다.
  • 02:00마다 만료된 초대장은 그냥 삭제하고 있는데 이것도 삭제하지 말고 따로 만료된 초대장이라고 보여주는 것이 나을지 고민입니다. (다시 초대장 보내기? 같은 버튼을 만들 수도 있을 것 같아서)
  • 오늘 일정이 있어 급하게 PR 올려 봅니다! 스케줄러 동작 부분은 아직 정상 동작하는지 확인하지 못했습니다..!
  • 이상한 부분이나 수정할 부분 피드백 해주시면 감사하겠습니다!!

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

Summary by CodeRabbit

릴리스 노트

  • New Features

    • 조직 대기 중인 멤버 목록 조회 API 추가
    • 초대 발송 시 중복 초대 처리(기존 초대 업데이트 및 5분 재전송 제한) 추가
    • 초대 수락 시 관련 대기 초대 기록 자동 제거
  • Bug Fixes

    • 만료된 초대 기록을 매일 자동으로 정리하는 스케줄러 추가
  • Documentation

    • 대기 멤버 조회 API에 대한 Swagger 문서 추가

@kingmingyu kingmingyu self-assigned this Mar 30, 2026
@kingmingyu kingmingyu added ✨ Feature 새로운 기능 추가 🗄️ DB 데이터베이스, 엔티티, 마이그레이션 관련 ♻️ Refactor 코드 구조 개선 labels Mar 30, 2026
@kingmingyu kingmingyu linked an issue Mar 30, 2026 that may be closed by this pull request
3 tasks
@coderabbitai
Copy link
Copy Markdown

coderabbitai bot commented Mar 30, 2026

Walkthrough

조직 초대의 영속화(OrgInvitation 엔티티)와 만료 관리 스케줄러를 추가하고, 조직별 초대 대기 멤버를 조회하는 API 및 관련 DTO/매퍼/서비스/컨트롤러를 구현했습니다.

Changes

Cohort / File(s) Summary
Persistence - Entity & Repo
src/.../organization/persistence/entity/OrgInvitation.java, src/.../organization/persistence/repository/OrgInvitationRepository.java
새로운 JPA 엔티티 OrgInvitation 추가(이메일, invitedAt, expireAt, organization FK) 및 repository에 만료 필터 조회, 이메일+조직 조회, 만료 삭제 메서드 추가(findByOrganizationIdAndExpireAtAfter, findByEmailAndOrganization, deleteByExpireAtBefore).
DTOs & Mapper
src/.../organization/application/dto/response/OrgResponse.java, src/.../organization/application/mapper/OrgConverter.java
OrgPendingMemberDTOOrgPendingMembersResponse 레코드 추가 (@JsonFormat(pattern="yyyy-MM-dd HH:mm") 지정). OrgConverter에 OrgInvitation 생성/단건·복수 DTO 변환 유틸 메서드 추가(toOrgInvitation, toOrgPendingMemberDTO, toOrgPendingMembersResponse).
Service - Query & Impl
src/.../organization/domain/service/OrgQueryService.java, src/.../organization/domain/service/OrgQueryServiceImpl.java
인터페이스에 getPendingMembers(userId, orgId) 추가 및 구현체에서 조직 존재/멤버 검증 후 유효한(미만료) 초대 목록 조회하고 DTO로 변환하여 반환하는 로직 추가.
Service - Application Logic
src/.../organization/domain/service/OrgServiceImpl.java
초대 전 파악용 findByEmailAndOrganization 조회 추가. 중복 초대 시 5분 이내 재전송 예외(ORG_ALREADY_INVITE) 처리, 기존 초대의 updateInvitedAt() 호출 또는 신규 저장. 수락 시 관련 초대 레코드 삭제 추가.
Scheduled Task
src/.../organization/domain/service/OrgInvitationScheduler.java
매일 02:00에 만료된 초대 삭제하는 @Scheduled 스케줄러 및 트랜잭셔널 정리 메서드 추가.
Presentation & Docs
src/.../organization/presentation/OrgController.java, src/.../organization/presentation/docs/OrgControllerDocs.java
GET /api/org/members/{orgId}/pending 엔드포인트 추가(인증된 userId로 호출). Swagger 문서(Docs 인터페이스)에도 메서드 서명 추가.
Error Codes
src/.../organization/exception/code/OrgErrorCode.java
새 에러코드 ORG_ALREADY_INVITE(409) 추가: "초대 메시지 재전송은 5분 이후 가능합니다."

Sequence Diagram

sequenceDiagram
    participant Client as Client
    participant Ctrl as OrgController
    participant QuerySvc as OrgQueryService
    participant OrgRepo as Organization Repository
    participant MemberRepo as OrgMember Repository
    participant InvRepo as OrgInvitation Repository
    participant Conv as OrgConverter
    participant DB as Database

    Client->>Ctrl: GET /api/org/members/{orgId}/pending
    Ctrl->>QuerySvc: getPendingMembers(userId, orgId)
    QuerySvc->>OrgRepo: findById(orgId)
    OrgRepo->>DB: SELECT organization WHERE id=?
    DB-->>OrgRepo: Organization
    QuerySvc->>MemberRepo: findActiveMember(userId, orgId)
    MemberRepo->>DB: SELECT member WHERE user_id=? AND org_id=? AND status=ACTIVE
    DB-->>MemberRepo: OrgMember
    QuerySvc->>InvRepo: findByOrganizationIdAndExpireAtAfter(orgId, now)
    InvRepo->>DB: SELECT invitations WHERE org_id=? AND expire_at>?
    DB-->>InvRepo: List<OrgInvitation>
    QuerySvc->>Conv: toOrgPendingMembersResponse(invitations)
    Conv-->>QuerySvc: OrgPendingMembersResponse
    QuerySvc-->>Ctrl: OrgPendingMembersResponse
    Ctrl-->>Client: 200 OK { pendingMembers: [...] }
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

  • Feat/#23 #24 — OrgResponse 및 OrgConverter 변경과 직접적으로 중첩되는 DTO/매퍼 수정 이력.
  • Feat/#27 조직 멤버 초대 및 수락 구현 #42 — 초대 흐름(전송/수락) 구현 관련으로 OrgInvitation 엔티티/서비스와 기능적으로 연관.
  • Feat/#19 #20 — OrgInvitation 및 조직 멤버 타입(OrgMember/OrgRole 등) 변경과 상호 의존성 가능성 있음.

Suggested reviewers

  • jinnieusLab
  • ojy0903

시니어 개발자 관점의 검토 의견

전반적으로 관심사 분리가 잘 되어 있고, 영속화된 초대 모델과 조회 API 구성이 명확합니다. 몇 가지 확인할 점만 짚습니다.

핵심 칭찬

  • OrgInvitation 엔티티와 Repository의 쿼리 네이밍이 명확합니다.
  • Scheduler가 정리 전용으로 분리된 점, 초대 만료를 24시간으로 관리하는 도메인 메서드(updateInvitedAt)가 일관적입니다.
  • DTO에 시간 포맷을 지정해 프론트가 다루기 쉽도록 한 점이 좋습니다.

주의/권장 사항

  1. 동시성 및 DB 제약
  • sendOrgInvitation에서 조회 후 분기하는 패턴은 동시성 레이스가 발생할 수 있습니다. 이메일+organization에 대해 DB 수준의 unique 제약을 추가하고, 예외 발생 시 재시도/업데이트로 보완하세요.
  1. N+1 위험
  • 현재 Converter가 invitation의 organization 필드를 사용하지 않더라도, 향후 확장 시 Lazy 로딩으로 N+1이 발생할 수 있습니다. 필요 시 fetch join 또는 @EntityGraph 고려.
  1. 스케줄러 타임존
  • @Scheduled(cron = "0 0 2 * * *")는 서버 타임존에 따라 실행됩니다. 운영 기준(예: UTC)과 일치시키려면 zone 옵션을 명시하세요.
  1. 에러 처리 세부화
  • ORG_ALREADY_INVITE 로직이 "5분" 체크를 서비스에서 처리하는데, 이를 상수화하고 단위 테스트로 경계 조건(정확히 5분) 커버리지를 권장합니다.

필요하면 동시성 충돌을 막는 DB 제약 DDL, Scheduler zone 설정 예시, 또는 테스트 케이스 스켈레톤을 짤 수 있도록 도와드릴게요.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 59.09% 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의 핵심 변경사항(초대 중인 맴버 조회 API 추가)을 명확하고 간결하게 요약하고 있습니다.
Description check ✅ Passed PR 설명이 템플릿을 따르고 있으며, 개요, 작업 내용, 테스트 결과, 체크리스트, 리뷰 포인트가 모두 포함되어 있습니다.
Linked Issues check ✅ Passed PR은 #107의 요구사항을 충족합니다. OrgInvitation 엔티티 추가, 초대 대기 맴버 조회 API 구현, 초대 메일 발송/수락 시 DB 저장/삭제 로직이 모두 포함되었습니다.
Out of Scope Changes check ✅ Passed 모든 변경사항이 #107 요구사항 범위 내에 있습니다. OrgInvitation 엔티티, 초대 조회 API, 스케줄러, 초대 로직 개선이 모두 명시된 목표를 지원합니다.

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

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/#107

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
Copy Markdown

@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 (7)
src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/OrgInvitation.java (2)

34-37: LocalDateTime.now() 중복 호출로 인한 시간 불일치 가능성

updateInvitedAt() 메서드에서 LocalDateTime.now()를 두 번 호출하고 있어서, invitedAtexpireAt 사이에 미세한 시간 차이가 발생할 수 있어요. 예를 들어 invitedAt이 09:00:00.000이고 expireAt이 다음날 09:00:00.001이 될 수 있습니다.

실제로는 큰 문제가 아니지만, 코드 일관성을 위해 하나의 시점을 사용하는 것이 좋습니다.

♻️ 개선 제안
 public void updateInvitedAt() {
-    this.invitedAt = LocalDateTime.now();
-    this.expireAt = LocalDateTime.now().plusHours(24);
+    LocalDateTime now = LocalDateTime.now();
+    this.invitedAt = now;
+    this.expireAt = now.plusHours(24);
 }
🤖 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/entity/OrgInvitation.java`
around lines 34 - 37, The updateInvitedAt() method in OrgInvitation calls
LocalDateTime.now() twice causing a tiny mismatch between invitedAt and
expireAt; fix by capturing LocalDateTime now = LocalDateTime.now() once inside
OrgInvitation.updateInvitedAt(), assign this.invitedAt = now and this.expireAt =
now.plusHours(24) so both fields are derived from the same instant.

21-22: email 컬럼 길이 제한 추가 권장

email 컬럼에 length 속성이 지정되지 않아 DB에서 기본값(보통 255)이 적용됩니다. PR 이미지의 ERD를 보면 VARCHAR(3200)으로 설정되어 있는데, RFC 5321 기준 이메일 최대 길이는 254자이므로 과도하게 큰 값입니다.

명시적으로 길이를 지정하면 DB 스키마와 코드의 일관성을 유지할 수 있어요.

♻️ 개선 제안
-@Column(nullable = false, name = "email")
+@Column(nullable = false, name = "email", length = 320)
 private String email;
🤖 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/entity/OrgInvitation.java`
around lines 21 - 22, The OrgInvitation entity's email field currently uses
`@Column`(nullable = false, name = "email") without a length, which leaves the DB
default (often 255) or mismatched ERDs; update the `@Column` on the email field in
the OrgInvitation class to explicitly set an appropriate length (e.g., length =
254 per RFC 5321/RFC 5322) and keep nullable = false; also ensure any related
migration or schema definition is updated to match this new length to keep code
and DB consistent.
src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgQueryServiceImpl.java (1)

83-89: 조직 존재 확인에 불필요한 엔티티 조회

organization 변수를 조회하지만 실제로 사용하지 않고 있어요. 단순히 존재 여부만 확인하는 경우 existsById를 사용하면 더 효율적입니다.

마찬가지로 requester도 존재 여부 확인용으로만 사용되고 있어서, 멤버 검증을 위한 별도의 exists 메서드가 있다면 활용할 수 있습니다.

♻️ 최적화 제안
 public OrgResponse.OrgPendingMembersResponse getPendingMembers(Long userId, Long orgId) {
     // 조직 존재 여부 확인
-    Organization organization = orgRepository.findById(orgId)
-            .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_NOT_FOUND));
+    if (!orgRepository.existsById(orgId)) {
+        throw new OrgHandler(OrgErrorCode.ORG_NOT_FOUND);
+    }

     // 해당 조직 맴버인지 검증
-    OrgMember requester = orgMemberRepository.findByUserIdAndOrgId(userId, orgId)
-            .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND));
+    if (orgMemberRepository.findByUserIdAndOrgId(userId, orgId).isEmpty()) {
+        throw new OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND);
+    }

     // 만료되지 않은 초대 내역 조회
     List<OrgInvitation> invitations = orgInvitationRepository.findByOrganizationIdAndExpireAtAfter(orgId, LocalDateTime.now());

     return OrgConverter.toOrgPendingMembersResponse(invitations);
 }
🤖 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/OrgQueryServiceImpl.java`
around lines 83 - 89, The code in OrgQueryServiceImpl currently calls
orgRepository.findById(orgId) and
orgMemberRepository.findByUserIdAndOrgId(userId, orgId) only to check existence;
replace these full-entity fetches with existence checks (e.g.,
orgRepository.existsById(orgId) and an
orgMemberRepository.existsByUserIdAndOrgId(userId, orgId) or add such a method)
and throw the same OrgHandler(OrgErrorCode.ORG_NOT_FOUND) /
OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND) when the boolean check fails;
remove unused variables (organization, requester) to avoid unnecessary entity
loading and improve performance in OrgQueryServiceImpl.
src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgInvitationScheduler.java (1)

19-27: 스케줄러 구현 적절합니다 👍

매일 02:00에 만료된 초대를 정리하는 스케줄러가 잘 구현되어 있어요. @EnableSchedulingWhereYouAdApplication.java에 이미 선언되어 있어서 정상 동작할 것입니다.

몇 가지 운영 관점 조언:

  1. 삭제된 건수 로깅: 몇 건이 삭제되었는지 로깅하면 모니터링에 도움이 됩니다.
  2. 예외 처리: 스케줄러 실행 중 예외 발생 시 로깅이 필요할 수 있어요.
🔧 운영 개선 제안 (선택사항)
 `@Scheduled`(cron = "0 0 2 * * *")
 `@Transactional`
 public void cleanupExpiredInvitations() {
     log.info("만료된 조직 초대 내역 삭제 스케줄러 실행");
-    // 현재 시간을 기준으로 만료된 데이터를 모두 삭제
-    orgInvitationRepository.deleteByExpireAtBefore(LocalDateTime.now());
-    log.info("만료된 조직 초대 데이터 정리");
+    try {
+        // 벌크 삭제 쿼리 사용 시 삭제 건수 반환 가능
+        orgInvitationRepository.deleteByExpireAtBefore(LocalDateTime.now());
+        log.info("만료된 조직 초대 데이터 정리 완료");
+    } catch (Exception e) {
+        log.error("만료된 조직 초대 데이터 정리 중 오류 발생", e);
+    }
 }
🤖 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/OrgInvitationScheduler.java`
around lines 19 - 27, Update OrgInvitationScheduler.cleanupExpiredInvitations to
capture and log the number of deleted records and to handle exceptions: call
orgInvitationRepository.deleteByExpireAtBefore in a try/catch, log the deleted
count (return value or count retrieved before/after deletion) with context in
the info log, and on exception log an error with the exception message/stack
trace so failures are recorded; ensure to keep the `@Scheduled` and `@Transactional`
annotations on the cleanupExpiredInvitations method.
src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgInvitationRepository.java (1)

19-20: 대량 삭제 시 성능 이슈 가능성

Spring Data JPA의 파생 삭제 메서드(deleteBy...)는 내부적으로 먼저 SELECT로 모든 엔티티를 조회한 후, 각각 DELETE를 실행합니다. 만료된 초대가 많을 경우 N개의 DELETE 쿼리가 발생해 성능 저하가 있을 수 있어요.

예를 들어 만료된 초대가 1000개라면:

  • 현재: 1 SELECT + 1000 DELETE 쿼리
  • 벌크 삭제: 1 DELETE 쿼리

운영 환경에서 초대 데이터가 많지 않다면 현재 방식도 괜찮지만, 확장성을 고려하면 벌크 삭제가 효율적입니다.

♻️ 벌크 삭제 쿼리 제안
+import org.springframework.data.jpa.repository.Modifying;
+import org.springframework.data.jpa.repository.Query;
+import org.springframework.data.repository.query.Param;

 public interface OrgInvitationRepository extends JpaRepository<OrgInvitation, Long> {
     // ... existing methods ...

-    // 만료일 기준 삭제
-    void deleteByExpireAtBefore(LocalDateTime now);
+    // 만료일 기준 벌크 삭제
+    `@Modifying`
+    `@Query`("DELETE FROM OrgInvitation i WHERE i.expireAt < :now")
+    void deleteByExpireAtBefore(`@Param`("now") LocalDateTime now);
 }
🤖 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/OrgInvitationRepository.java`
around lines 19 - 20, The derived repository method
deleteByExpireAtBefore(LocalDateTime now) in OrgInvitationRepository can trigger
N individual DELETEs after a SELECT; replace it with a bulk delete using a
single executing query: add a new repository method annotated with `@Modifying`
and `@Query` (JPQL or native) that performs DELETE FROM OrgInvitation o WHERE
o.expireAt < :now, and ensure the method is executed in a transactional context
(`@Transactional` on the method or service) to allow the bulk operation; remove or
deprecate the current deleteByExpireAtBefore signature to avoid accidental
usage.
src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.java (1)

104-112: 만료 시간 계산 로직 중복 (DRY 위반)

toOrgInvitation에서 24시간 만료 계산 로직이 OrgInvitation.updateInvitedAt()과 중복됩니다. 만약 만료 시간이 24시간에서 48시간으로 변경된다면 두 곳을 모두 수정해야 해요.

또한 여기서도 LocalDateTime.now()를 두 번 호출하고 있어서 시간 불일치가 발생할 수 있습니다.

♻️ 중복 제거 제안

상수를 정의하거나, 빌더 생성 후 updateInvitedAt()을 호출하는 방식을 고려해보세요:

 public static OrgInvitation toOrgInvitation(String email, Organization organization) {
-    return OrgInvitation.builder()
+    LocalDateTime now = LocalDateTime.now();
+    return OrgInvitation.builder()
             .email(email)
             .organization(organization)
-            .invitedAt(java.time.LocalDateTime.now())
-            .expireAt(java.time.LocalDateTime.now().plusHours(24))
+            .invitedAt(now)
+            .expireAt(now.plusHours(24))
             .build();
 }

더 나아가, 만료 기간을 상수나 설정값으로 추출하면 유지보수가 쉬워집니다:

// OrgInvitation 엔티티나 별도 상수 클래스에
public static final int INVITATION_EXPIRE_HOURS = 24;
🤖 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/mapper/OrgConverter.java`
around lines 104 - 112, The toOrgInvitation method duplicates expire-time logic
and calls LocalDateTime.now() twice; introduce a single source of truth (e.g.,
add public static final int INVITATION_EXPIRE_HOURS = 24 to OrgInvitation) and
compute the timestamp once, or build the OrgInvitation and then call
OrgInvitation.updateInvitedAt() to set invitedAt/expireAt consistently; update
toOrgInvitation to use the new INVITATION_EXPIRE_HOURS constant (or the
updateInvitedAt helper) and a single LocalDateTime now value so change only
needs to be made in one place and both fields use the same timestamp.
src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java (1)

178-181: 인증/인가 실패 응답도 API 문서에 명시하는 것을 권장합니다.

@AuthenticationPrincipal 기반 엔드포인트라면 401/403 케이스를 함께 문서화해두는 편이 클라이언트 연동 시 안전합니다.

As per coding guidelines, "SOLID 원칙, 의존성 주입(DI), 예외 처리(GlobalExceptionHandler)가 적절한지 보라."

🤖 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 178 - 181, This API docs block only documents 200 and 404 but omits
authentication/authorization failures; update the `@ApiResponses` on the Org
controller docs for endpoints using `@AuthenticationPrincipal` to add entries for
401 (unauthenticated) and 403 (forbidden) so clients know auth failure cases,
e.g., add `@ApiResponse`(responseCode="401", description="인증 필요") and
`@ApiResponse`(responseCode="403", description="권한 없음") alongside existing 200/404
entries in OrgControllerDocs (the doc annotations for the methods referencing
`@AuthenticationPrincipal`); ensure the descriptions match your
GlobalExceptionHandler messages for consistency.
🤖 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/presentation/docs/OrgControllerDocs.java`:
- Around line 175-176: Fix the Korean typo "맴버" → "멤버" in the Swagger API doc
strings in OrgControllerDocs: update the summary and description texts (the
`@Operation` or annotation attributes that currently read "초대 대기 중인 맴버 조회 API" and
"조직에 초대 대기 중인 맴버 리스트를 조회합니다...") to use "멤버" so both the summary and description
match correct spelling.

---

Nitpick comments:
In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.java`:
- Around line 104-112: The toOrgInvitation method duplicates expire-time logic
and calls LocalDateTime.now() twice; introduce a single source of truth (e.g.,
add public static final int INVITATION_EXPIRE_HOURS = 24 to OrgInvitation) and
compute the timestamp once, or build the OrgInvitation and then call
OrgInvitation.updateInvitedAt() to set invitedAt/expireAt consistently; update
toOrgInvitation to use the new INVITATION_EXPIRE_HOURS constant (or the
updateInvitedAt helper) and a single LocalDateTime now value so change only
needs to be made in one place and both fields use the same timestamp.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgInvitationScheduler.java`:
- Around line 19-27: Update OrgInvitationScheduler.cleanupExpiredInvitations to
capture and log the number of deleted records and to handle exceptions: call
orgInvitationRepository.deleteByExpireAtBefore in a try/catch, log the deleted
count (return value or count retrieved before/after deletion) with context in
the info log, and on exception log an error with the exception message/stack
trace so failures are recorded; ensure to keep the `@Scheduled` and `@Transactional`
annotations on the cleanupExpiredInvitations method.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgQueryServiceImpl.java`:
- Around line 83-89: The code in OrgQueryServiceImpl currently calls
orgRepository.findById(orgId) and
orgMemberRepository.findByUserIdAndOrgId(userId, orgId) only to check existence;
replace these full-entity fetches with existence checks (e.g.,
orgRepository.existsById(orgId) and an
orgMemberRepository.existsByUserIdAndOrgId(userId, orgId) or add such a method)
and throw the same OrgHandler(OrgErrorCode.ORG_NOT_FOUND) /
OrgHandler(OrgErrorCode.ORG_MEMBER_NOT_FOUND) when the boolean check fails;
remove unused variables (organization, requester) to avoid unnecessary entity
loading and improve performance in OrgQueryServiceImpl.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/OrgInvitation.java`:
- Around line 34-37: The updateInvitedAt() method in OrgInvitation calls
LocalDateTime.now() twice causing a tiny mismatch between invitedAt and
expireAt; fix by capturing LocalDateTime now = LocalDateTime.now() once inside
OrgInvitation.updateInvitedAt(), assign this.invitedAt = now and this.expireAt =
now.plusHours(24) so both fields are derived from the same instant.
- Around line 21-22: The OrgInvitation entity's email field currently uses
`@Column`(nullable = false, name = "email") without a length, which leaves the DB
default (often 255) or mismatched ERDs; update the `@Column` on the email field in
the OrgInvitation class to explicitly set an appropriate length (e.g., length =
254 per RFC 5321/RFC 5322) and keep nullable = false; also ensure any related
migration or schema definition is updated to match this new length to keep code
and DB consistent.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgInvitationRepository.java`:
- Around line 19-20: The derived repository method
deleteByExpireAtBefore(LocalDateTime now) in OrgInvitationRepository can trigger
N individual DELETEs after a SELECT; replace it with a bulk delete using a
single executing query: add a new repository method annotated with `@Modifying`
and `@Query` (JPQL or native) that performs DELETE FROM OrgInvitation o WHERE
o.expireAt < :now, and ensure the method is executed in a transactional context
(`@Transactional` on the method or service) to allow the bulk operation; remove or
deprecate the current deleteByExpireAtBefore signature to avoid accidental
usage.

In
`@src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java`:
- Around line 178-181: This API docs block only documents 200 and 404 but omits
authentication/authorization failures; update the `@ApiResponses` on the Org
controller docs for endpoints using `@AuthenticationPrincipal` to add entries for
401 (unauthenticated) and 403 (forbidden) so clients know auth failure cases,
e.g., add `@ApiResponse`(responseCode="401", description="인증 필요") and
`@ApiResponse`(responseCode="403", description="권한 없음") alongside existing 200/404
entries in OrgControllerDocs (the doc annotations for the methods referencing
`@AuthenticationPrincipal`); ensure the descriptions match your
GlobalExceptionHandler messages for consistency.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 2a993e0b-5122-4cf1-93ad-ca8755f75eee

📥 Commits

Reviewing files that changed from the base of the PR and between 2fb9654 and fabbcbc.

📒 Files selected for processing (10)
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/response/OrgResponse.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgInvitationScheduler.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgQueryService.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgQueryServiceImpl.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/OrgInvitation.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgInvitationRepository.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.java
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java

Comment on lines +175 to +176
summary = "초대 대기 중인 맴버 조회 API",
description = "조직에 초대 대기 중인 맴버 리스트를 조회합니다. 해당 맴버의 이메일과 초대 시작일, 만료일을 반환합니다."
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟡 Minor

문서 오탈자(맴버멤버)를 수정해주세요.

Swagger 설명 텍스트에 오탈자가 있어 API 문서 신뢰도를 떨어뜨립니다.

✏️ 제안 수정안
-            summary = "초대 대기 중인 맴버 조회 API",
-            description = "조직에 초대 대기 중인 맴버 리스트를 조회합니다. 해당 맴버의 이메일과 초대 시작일, 만료일을 반환합니다."
+            summary = "초대 대기 중인 멤버 조회 API",
+            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
summary = "초대 대기 중인 맴버 조회 API",
description = "조직에 초대 대기 중인 맴버 리스트를 조회합니다. 해당 맴버의 이메일과 초대 시작일, 만료일을 반환합니다."
summary = "초대 대기 중인 멤버 조회 API",
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 175 - 176, Fix the Korean typo "맴버" → "멤버" in the Swagger API doc
strings in OrgControllerDocs: update the summary and description texts (the
`@Operation` or annotation attributes that currently read "초대 대기 중인 맴버 조회 API" and
"조직에 초대 대기 중인 맴버 리스트를 조회합니다...") to use "멤버" so both the summary and description
match correct spelling.

Copy link
Copy Markdown
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: 고생하셨습니다!
제 생각에는 만료된 초대장을 지우지 않으면 계속해서 초대장이 DB 에 남아있는 경우도 있을 거 같아서... 지우는게 좋아보이긴 합니다!
스케줄러 쪽은 제가 보기에는 깔끔하게 잘 작성된 거 같아요...
그리고 추가로 OrgInvitationRepository 쪽에 수정사항 하나 남겨봤는데 이건 필수는 아니고 선택사항인 것 같아서 빠르게 완료 해야한다면 그냥 넘겨도 괜찮을 것 같습니다!

Optional<OrgInvitation> findByEmailAndOrganization(String email, Organization organization);

// 만료일 기준 삭제
void deleteByExpireAtBefore(LocalDateTime now);
Copy link
Copy Markdown
Collaborator

@ojy0903 ojy0903 Mar 30, 2026

Choose a reason for hiding this comment

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

P4: 찾아보니까 해당 만료 초대장 지우는 메서드에서 JPA 메서드에 따라 정상 동작하지만, 만약 만료된 초대장이 많은 경우에는 아래처럼 Bulk 로 지우는 메서드를 직접 작성해도 서버 성능에 좋다고 합니다! 동작하는거에 차이는 없어서 반영하지 않아도 괜찮을 것 같긴합니다...

    @Modifying
    @Query("DELETE FROM OrgInvitation ov WHERE ov.expireAt < :now")
    void deleteByExpireAtBefore(@Param("now") LocalDateTime now);

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

헉 찾아보니까 이게 1차 캐시를 거치지 않고 쿼리 1개로 바로 DB로 가서 성능이 훨씬 좋다고 하네요..! 이걸로 바꾸겠습니다!! 감사합니다 👍

Copy link
Copy Markdown
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.

P4: 고생하셨습니다! 조직 멤버 초대 로직을 제가 짰던 터라 코드 리팩토링이 쉽지 않으셨을 수 있는데, 깔끔하게 잘 구현해주신 것 같아요..! (생각해보니 제가 ERD의 OrgInvitation 엔티티 구현도 잊었었네요...)

그리고 오전 2시마다 만료된 초대장을 삭제하는 것 좋다고 생각합니다! 삭제하지 않고 남겨두기엔 Redis에서도 메일을 24시간 만료 시 삭제하고 있어서 같은 흐름으로 db에서도 삭제하도록 맞추는 게 맞을 듯하고, 만료된 걸 모두 보관하기엔 db 낭비가 있을 것 같다고 생각합니다!

// 신규 초대 테이블 저장
OrgInvitation newInvitation = OrgConverter.toOrgInvitation(email, organization);
orgInvitationRepository.save(newInvitation);
}
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

기존 코드와 잘 합쳐져, 조직 멤버인지 확인 > 대기 중인지 확인 > 초대 로직이 잘 이어진 거 같아 좋습니다!

다만 현재 초대 수락 대기 중에 중복 초대를 하는 경우, 이미 초대 메일이 갔어도 무제한으로 초대 메일을 보낼 수 있는 상황이라 5분 정도의 재초대 제한을 두는 에러 메시지를 반환하는 건 어떨까요? (ex. 초대 메시지 재전송은 최소 5분 이후 가능합니다.) 초대 시점으로부터의 시간을 측정해 5분이 지났다면 재초대가 가능하게끔 하면 좋을 것 같습니다!

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

그 부분까지는 제가 생각하지 못했네요..! 시간 제한 두는 방향으로 리펙터링 하겠습니다 감사합니다!! 👍

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

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

image

Copy link
Copy Markdown

@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

🤖 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 406-423: Add a DB- and entity-level unique constraint on
(organization, email) for OrgInvitation and update the OrgServiceImpl flow to
handle concurrent inserts: keep the existing find/update path using
orgInvitationRepository and OrgConverter.toOrgInvitation, but when saving a new
OrgInvitation catch the repository/DB unique-violation exception (e.g.,
DataIntegrityViolationException or the JPA-specific exception) and translate it
into throwing new OrgHandler(OrgErrorCode.ORG_ALREADY_INVITE); this ensures race
conditions producing duplicate rows are mapped to the same error and preserves
the “single pending invitation” invariant.
- Around line 468-470: The current use of
orgInvitationRepository.findByEmailAndOrganization(...).ifPresent(...) allows
acceptance when the DB invitation row is missing; change this to explicitly
require the DB row: call
orgInvitationRepository.findByEmailAndOrganization(email, organization), if the
Optional is empty throw the ORG_INVITATION_INVALID error (or corresponding
exception), otherwise proceed and delete the found OrgInvitation (use the
retrieved entity with orgInvitationRepository.delete(foundInvitation)); ensure
this logic replaces the current ifPresent usage in OrgServiceImpl so acceptance
fails when the DB row is absent.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Pro

Run ID: 21a59cb7-4131-4343-b247-761624c12e65

📥 Commits

Reviewing files that changed from the base of the PR and between fabbcbc and 1d7b0b7.

📒 Files selected for processing (3)
  • 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/persistence/repository/OrgInvitationRepository.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgInvitationRepository.java

Comment on lines +406 to +423
// 이미 동일한 이메일로 대기 중인 초대가 있는지 확인
Optional<OrgInvitation> existingInvitation = orgInvitationRepository.findByEmailAndOrganization(email, organization);
if (existingInvitation.isPresent()) {

LocalDateTime inviteAt = existingInvitation.get().getInvitedAt();
LocalDateTime now = LocalDateTime.now();

// 초대 간격 5분 설정
if(Duration.between(inviteAt, now).toMinutes() < 5){
throw new OrgHandler(OrgErrorCode.ORG_ALREADY_INVITE);
}
// 5분 이상의 중복 초대의 경우 갱신만 진행(만료 시간도 같이 갱신)
existingInvitation.get().updateInvitedAt();
} else {
// 신규 초대 테이블 저장
OrgInvitation newInvitation = OrgConverter.toOrgInvitation(email, organization);
orgInvitationRepository.save(newInvitation);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

email + organization 단건 전제가 DB에서 보장되지 않습니다.

기존 row를 갱신해서 대기 목록을 한 건으로 유지하려는 방향은 좋습니다. 다만 Line 407은 이 조합이 항상 1건이라고 가정하는데, 현재 OrgInvitation에는 (org_id, email) 유니크 제약이 없습니다. 두 요청이 거의 동시에 들어오면 둘 다 Line 421로 내려가 중복 row가 생길 수 있고, 이후 이 단건 조회와 Line 469 삭제 로직이 같이 깨질 수 있습니다. DB/엔티티에 유니크 제약을 추가하고, 저장 충돌은 ORG_ALREADY_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/domain/service/OrgServiceImpl.java`
around lines 406 - 423, Add a DB- and entity-level unique constraint on
(organization, email) for OrgInvitation and update the OrgServiceImpl flow to
handle concurrent inserts: keep the existing find/update path using
orgInvitationRepository and OrgConverter.toOrgInvitation, but when saving a new
OrgInvitation catch the repository/DB unique-violation exception (e.g.,
DataIntegrityViolationException or the JPA-specific exception) and translate it
into throwing new OrgHandler(OrgErrorCode.ORG_ALREADY_INVITE); this ensures race
conditions producing duplicate rows are mapped to the same error and preserves
the “single pending invitation” invariant.

Comment on lines +468 to +470
// OrgInvitation 에서 해당 초대 내역이 있는지 확인 후 삭제 (대기열 제거)
orgInvitationRepository.findByEmailAndOrganization(email, organization)
.ifPresent(orgInvitationRepository::delete);
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

초대 레코드가 없어도 수락이 성공합니다.

Line 469를 ifPresent로 두면 Redis 토큰만 살아 있으면 OrgInvitation row가 없어도 멤버 편입이 진행됩니다. 초대 플로우가 Redis와 DB를 별도로 다루고 있고 만료 삭제 스케줄러도 개입하므로, 둘이 어긋나는 순간 대기 멤버 조회에는 없는 초대를 수락할 수 있습니다. 여기서는 초대 row를 필수 검증 대상으로 보고, 없으면 ORG_INVITATION_INVALID를 던지는 편이 더 안전합니다.

💡 제안 코드
-        orgInvitationRepository.findByEmailAndOrganization(email, organization)
-                .ifPresent(orgInvitationRepository::delete);
+        OrgInvitation invitation = orgInvitationRepository.findByEmailAndOrganization(email, organization)
+                .orElseThrow(() -> new OrgHandler(OrgErrorCode.ORG_INVITATION_INVALID));
+        orgInvitationRepository.delete(invitation);
🤖 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 468 - 470, The current use of
orgInvitationRepository.findByEmailAndOrganization(...).ifPresent(...) allows
acceptance when the DB invitation row is missing; change this to explicitly
require the DB row: call
orgInvitationRepository.findByEmailAndOrganization(email, organization), if the
Optional is empty throw the ORG_INVITATION_INVALID error (or corresponding
exception), otherwise proceed and delete the found OrgInvitation (use the
retrieved entity with orgInvitationRepository.delete(foundInvitation)); ensure
this logic replaces the current ifPresent usage in OrgServiceImpl so acceptance
fails when the DB row is absent.

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

Labels

🗄️ DB 데이터베이스, 엔티티, 마이그레이션 관련 ✨ Feature 새로운 기능 추가 ♻️ Refactor 코드 구조 개선

Projects

None yet

Development

Successfully merging this pull request may close these issues.

feat: 조직 맴버 조회 - 초대 중인 맴버 조회 API

3 participants