Feat/#107 조직 맴버 조회 - 초대 중인 맴버 조회 API#108
Conversation
Walkthrough조직 초대의 영속화(OrgInvitation 엔티티)와 만료 관리 스케줄러를 추가하고, 조직별 초대 대기 멤버를 조회하는 API 및 관련 DTO/매퍼/서비스/컨트롤러를 구현했습니다. Changes
Sequence DiagramsequenceDiagram
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: [...] }
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
시니어 개발자 관점의 검토 의견전반적으로 관심사 분리가 잘 되어 있고, 영속화된 초대 모델과 조회 API 구성이 명확합니다. 몇 가지 확인할 점만 짚습니다. 핵심 칭찬
주의/권장 사항
필요하면 동시성 충돌을 막는 DB 제약 DDL, Scheduler zone 설정 예시, 또는 테스트 케이스 스켈레톤을 짤 수 있도록 도와드릴게요. 🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
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()를 두 번 호출하고 있어서,invitedAt과expireAt사이에 미세한 시간 차이가 발생할 수 있어요. 예를 들어 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 컬럼 길이 제한 추가 권장
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에 만료된 초대를 정리하는 스케줄러가 잘 구현되어 있어요.
@EnableScheduling이WhereYouAdApplication.java에 이미 선언되어 있어서 정상 동작할 것입니다.몇 가지 운영 관점 조언:
- 삭제된 건수 로깅: 몇 건이 삭제되었는지 로깅하면 모니터링에 도움이 됩니다.
- 예외 처리: 스케줄러 실행 중 예외 발생 시 로깅이 필요할 수 있어요.
🔧 운영 개선 제안 (선택사항)
`@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
📒 Files selected for processing (10)
src/main/java/com/whereyouad/WhereYouAd/domains/organization/application/dto/response/OrgResponse.javasrc/main/java/com/whereyouad/WhereYouAd/domains/organization/application/mapper/OrgConverter.javasrc/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgInvitationScheduler.javasrc/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgQueryService.javasrc/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgQueryServiceImpl.javasrc/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.javasrc/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/entity/OrgInvitation.javasrc/main/java/com/whereyouad/WhereYouAd/domains/organization/persistence/repository/OrgInvitationRepository.javasrc/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/OrgController.javasrc/main/java/com/whereyouad/WhereYouAd/domains/organization/presentation/docs/OrgControllerDocs.java
| summary = "초대 대기 중인 맴버 조회 API", | ||
| description = "조직에 초대 대기 중인 맴버 리스트를 조회합니다. 해당 맴버의 이메일과 초대 시작일, 만료일을 반환합니다." |
There was a problem hiding this comment.
문서 오탈자(맴버 → 멤버)를 수정해주세요.
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.
| 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.
ojy0903
left a comment
There was a problem hiding this comment.
P4: 고생하셨습니다!
제 생각에는 만료된 초대장을 지우지 않으면 계속해서 초대장이 DB 에 남아있는 경우도 있을 거 같아서... 지우는게 좋아보이긴 합니다!
스케줄러 쪽은 제가 보기에는 깔끔하게 잘 작성된 거 같아요...
그리고 추가로 OrgInvitationRepository 쪽에 수정사항 하나 남겨봤는데 이건 필수는 아니고 선택사항인 것 같아서 빠르게 완료 해야한다면 그냥 넘겨도 괜찮을 것 같습니다!
| Optional<OrgInvitation> findByEmailAndOrganization(String email, Organization organization); | ||
|
|
||
| // 만료일 기준 삭제 | ||
| void deleteByExpireAtBefore(LocalDateTime now); |
There was a problem hiding this comment.
P4: 찾아보니까 해당 만료 초대장 지우는 메서드에서 JPA 메서드에 따라 정상 동작하지만, 만약 만료된 초대장이 많은 경우에는 아래처럼 Bulk 로 지우는 메서드를 직접 작성해도 서버 성능에 좋다고 합니다! 동작하는거에 차이는 없어서 반영하지 않아도 괜찮을 것 같긴합니다...
@Modifying
@Query("DELETE FROM OrgInvitation ov WHERE ov.expireAt < :now")
void deleteByExpireAtBefore(@Param("now") LocalDateTime now);There was a problem hiding this comment.
헉 찾아보니까 이게 1차 캐시를 거치지 않고 쿼리 1개로 바로 DB로 가서 성능이 훨씬 좋다고 하네요..! 이걸로 바꾸겠습니다!! 감사합니다 👍
jinnieusLab
left a comment
There was a problem hiding this comment.
P4: 고생하셨습니다! 조직 멤버 초대 로직을 제가 짰던 터라 코드 리팩토링이 쉽지 않으셨을 수 있는데, 깔끔하게 잘 구현해주신 것 같아요..! (생각해보니 제가 ERD의 OrgInvitation 엔티티 구현도 잊었었네요...)
그리고 오전 2시마다 만료된 초대장을 삭제하는 것 좋다고 생각합니다! 삭제하지 않고 남겨두기엔 Redis에서도 메일을 24시간 만료 시 삭제하고 있어서 같은 흐름으로 db에서도 삭제하도록 맞추는 게 맞을 듯하고, 만료된 걸 모두 보관하기엔 db 낭비가 있을 것 같다고 생각합니다!
| // 신규 초대 테이블 저장 | ||
| OrgInvitation newInvitation = OrgConverter.toOrgInvitation(email, organization); | ||
| orgInvitationRepository.save(newInvitation); | ||
| } |
There was a problem hiding this comment.
기존 코드와 잘 합쳐져, 조직 멤버인지 확인 > 대기 중인지 확인 > 초대 로직이 잘 이어진 거 같아 좋습니다!
다만 현재 초대 수락 대기 중에 중복 초대를 하는 경우, 이미 초대 메일이 갔어도 무제한으로 초대 메일을 보낼 수 있는 상황이라 5분 정도의 재초대 제한을 두는 에러 메시지를 반환하는 건 어떨까요? (ex. 초대 메시지 재전송은 최소 5분 이후 가능합니다.) 초대 시점으로부터의 시간을 측정해 5분이 지났다면 재초대가 가능하게끔 하면 좋을 것 같습니다!
There was a problem hiding this comment.
그 부분까지는 제가 생각하지 못했네요..! 시간 제한 두는 방향으로 리펙터링 하겠습니다 감사합니다!! 👍
There was a problem hiding this comment.
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
📒 Files selected for processing (3)
src/main/java/com/whereyouad/WhereYouAd/domains/organization/domain/service/OrgServiceImpl.javasrc/main/java/com/whereyouad/WhereYouAd/domains/organization/exception/code/OrgErrorCode.javasrc/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
| // 이미 동일한 이메일로 대기 중인 초대가 있는지 확인 | ||
| 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); | ||
| } |
There was a problem hiding this comment.
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.
| // OrgInvitation 에서 해당 초대 내역이 있는지 확인 후 삭제 (대기열 제거) | ||
| orgInvitationRepository.findByEmailAndOrganization(email, organization) | ||
| .ifPresent(orgInvitationRepository::delete); |
There was a problem hiding this comment.
초대 레코드가 없어도 수락이 성공합니다.
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.

📌 관련 이슈
🚀 개요
📄 작업 내용
OrgInvitation추가, 기존 erdCloud에 존재하던 엔티티)+필요한 속성로 간소화했습니다.
📸 스크린샷 / 테스트 결과 (선택)
✅ 체크리스트
🔍 리뷰 포인트 (Review Points)
Summary by CodeRabbit
릴리스 노트
New Features
Bug Fixes
Documentation