Conversation
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthrough워크스페이스 초대 및 참여 요청 기능을 추가합니다. 사용자·관리자 컨트롤러와 스펙, DTO, 도메인 엔티티·상태, 인바운드/아웃바운드 포트 및 JPA/QueryDSL 구현, 유스케이스, 알림 이벤트·리스너·상수, 그리고 .gitignore 변경이 포함됩니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User as 사용자
participant UserCtrl as UserWorkspaceInvitationController
participant AcceptUC as AcceptWorkspaceInvitation
participant InvQuery as BusinessInvitationQueryRepository
participant WorkerUC as CreateWorkspaceWorkerUseCase
User->>UserCtrl: acceptInvitation(invitationId)
UserCtrl->>AcceptUC: execute(actor, invitationId)
AcceptUC->>InvQuery: findById(invitationId)
InvQuery-->>AcceptUC: BusinessInvitation
AcceptUC->>AcceptUC: 수신자/만료 검사 및 invitation.accept()
AcceptUC->>WorkerUC: addWorkerToWorkspace(...)
WorkerUC-->>AcceptUC: 완료
AcceptUC-->>UserCtrl: 성공
UserCtrl-->>User: 200 OK
sequenceDiagram
participant Manager as 관리자
participant ManagerCtrl as ManagerWorkspaceInvitationController
participant SendUC as SendWorkspaceInvitation
participant UserRepo as UserQueryRepository
participant InvRepo as BusinessInvitationRepository
participant Noti as FcmNotificationEventListener
Manager->>ManagerCtrl: sendInvitation(workspaceId, phoneNumbers)
ManagerCtrl->>SendUC: execute(actor, workspaceId, phoneNumbers)
loop 각 전화번호
SendUC->>UserRepo: findByContactIn(...)
alt 미등록
SendUC->>SendUC: unregisteredPhoneNumbers 추가
else 이미 근무자
SendUC->>SendUC: alreadyWorkerPhoneNumbers 추가
else 이미 초대됨
SendUC->>SendUC: alreadyInvitedPhoneNumbers 추가
else 신규
SendUC->>InvRepo: saveAll(new Invitations)
SendUC->>Noti: publish FcmNotificationEvent per invitation
end
end
SendUC-->>ManagerCtrl: SendWorkspaceInvitationResultDto
ManagerCtrl-->>Manager: 201 Created
sequenceDiagram
participant User as 사용자
participant UserCtrl as UserWorkspaceInvitationController
participant SendJoinUC as SendJoinRequest
participant JoinReqRepo as BusinessJoinRequestRepository
participant Noti as FcmNotificationEventListener
participant Manager as 관리자
participant ApproveUC as ApproveJoinRequest
participant WorkerUC as CreateWorkspaceWorkerUseCase
User->>UserCtrl: sendJoinRequest(workspaceId)
UserCtrl->>SendJoinUC: execute(actor, workspaceId)
SendJoinUC->>JoinReqRepo: save(new JoinRequest)
SendJoinUC->>Noti: publish request event
SendJoinUC-->>UserCtrl: 201 Created
Manager->>ApproveUC: approveJoinRequest(workspaceId, requestId)
ApproveUC->>ApproveUC: 권한/소속 검증 및 joinRequest.approve()
ApproveUC->>WorkerUC: addWorkerToWorkspace(...)
ApproveUC->>Noti: publish approval event
ApproveUC-->>Manager: 200 OK
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Suggested reviewers
🚥 Pre-merge checks | ✅ 2 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing Touches🧪 Generate unit tests (beta)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 17
🤖 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/dreamteam/alter/adapter/inbound/general/workspace/controller/UserWorkspaceInvitationController.java`:
- Around line 19-24: The controller class UserWorkspaceInvitationController is
missing the required Lombok `@Getter` per project guidelines; add the `@Getter`
annotation to the class declaration alongside the existing
`@RequiredArgsConstructor` (and do not add any `@Setter`). Locate the
UserWorkspaceInvitationController class and annotate it with `@Getter` so it
conforms to the "Controllers use `@Getter`, `@RequiredArgsConstructor` (no `@Setter`)"
rule.
In
`@src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/SendWorkspaceInvitationRequestDto.java`:
- Around line 13-14: SendWorkspaceInvitationRequestDto의 phoneNumbers 필드는 현재
`@NotEmpty로만` 검증하므로 각 전화번호의 형식 검증을 추가하세요: phoneNumbers 필드(또는 요소 타입)에 전화번호 정규식 기반
검증(`@Pattern`) 또는 커스텀 제약 어노테이션을 적용하거나, List<String> 대신 PhoneNumberDto(안에 `@Pattern이`
있는 필드)로 감싸서 각 요소가 유효한 포맷인지 검사하도록 변경하고, 관련 import와 유효성 검사 메시지를 함께 업데이트하세요.
In
`@src/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceInvitationController.java`:
- Around line 12-26: The controller class ManagerWorkspaceInvitationController
is missing the required `@Getter` annotation per coding guidelines; update the
class declaration that currently has `@RequiredArgsConstructor` to also include
Lombok's `@Getter` (keeping `@RequiredArgsConstructor` and no `@Setter`) so the
controller exposes generated getters for its final fields—ensure you import
lombok.Getter and place `@Getter` on the class alongside `@RequiredArgsConstructor`
and `@Validated`.
In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/RejectJoinRequest.java`:
- Around line 48-59: The sendNotification call inside the `@Transactional` use
case breaks transaction-notification consistency; move the notification logic
out of the transactional boundary in RejectJoinRequest by removing direct
notificationService.sendNotification(...) from the transactional method and
instead publish an asynchronous event or create an outbox record (e.g., via
ApplicationEventPublisher with a JoinRequestRejectedEvent or an Outbox entity)
that a separate non-transactional listener will consume to call
notificationService.sendNotification; ensure you reference the existing symbols
(RejectJoinRequest, sendNotification, notificationService,
JoinRequestRejectedEvent or Outbox) and keep notification sending in an async
listener or outbox dispatcher so DB commit completes before FCM is invoked.
In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/SendJoinRequest.java`:
- Around line 41-46: The existsPendingRequest + save sequence is vulnerable to
TOCTOU races; add a DB-level uniqueness constraint for pending join requests
(e.g., unique index on workspace_id + user_id for status = PENDING or equivalent
partial index) and keep the application check (BusinessJoinRequest.create /
existsPendingRequest) but also wrap
businessJoinRequestRepository.save(joinRequest) to catch persistence/constraint
violations (e.g., DataIntegrityViolationException or the ORM-specific exception)
and translate them to throw new CustomException(ErrorCode.CONFLICT, "...이미 합류
요청이 진행 중인 업장입니다.") so concurrent inserts result in a CONFLICT rather than
duplicate PENDING rows.
- Around line 48-60: The notification send inside SendJoinRequest is executed
within the transaction and must be moved out to avoid data/notification
mismatch; remove the direct notificationService.sendNotification call from the
transactional flow in SendJoinRequest and instead publish a post-commit
asynchronous event (e.g., emit a JoinRequestCreated event via an
ApplicationEventPublisher or persist an Outbox record) containing managerUserId,
TokenScope.MANAGER, title and body, then handle that event (or outbox processor)
to call notificationService.sendNotification; update SendJoinRequest to only
create/persist state and publish the event/outbox entry so notifications are
sent only after the transaction commits.
In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java`:
- Around line 73-79: The code currently calls
sendInvitationNotification(workspace, invitedUser) inside the loop immediately
after businessInvitationRepository.save(BusinessInvitation.create(...)) which
can cause consistency issues if the surrounding transaction later rolls back;
instead, stop sending notifications inside the DB transaction: collect the
created BusinessInvitation (or a lightweight DTO with workspace and invitedUser)
into a local list during the loop (alongside incrementing successCount), commit
the transaction, then after commit iterate that list to call
sendInvitationNotification or publish domain events; alternatively, implement an
outbox/event publish inside the same transaction (e.g., persist an OutboxEvent
entity when creating BusinessInvitation and have an async dispatcher emission
happen post-commit) so that notification sending occurs only after a successful
commit.
- Around line 68-75: The existsPendingInvitation check + save in
SendWorkspaceInvitation is vulnerable to TOCTOU races; add a uniqueness
constraint at the persistence layer (e.g., DB unique index covering workspace +
invitedUser + status=PENDING or equivalent) and change the save path to be
resilient to conflicts: attempt to persist the BusinessInvitation
(BusinessInvitation.create(...) and businessInvitationRepository.save(...))
inside a try/catch that catches the persistence/constraint exception (e.g.,
DataIntegrityViolationException / PersistenceException), and on conflict treat
it as "already invited" (add phoneNumber to alreadyInvitedPhoneNumbers and
continue) instead of failing; optionally wrap in a transaction or use
saveAndFlush to ensure immediate constraint enforcement.
In
`@src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessInvitation.java`:
- Around line 56-63: The factory method BusinessInvitation.create is missing
null checks for required associations; update BusinessInvitation.create to
validate inputs (workspace, invitedUser, invitedBy — and any other non-null
invariants like status/expiresAt if applicable) at the start of the method
(e.g., using Objects.requireNonNull or explicit checks) and throw a clear
runtime exception (IllegalArgumentException/NullPointerException) when a
required parameter is null so the built entity always satisfies its invariants;
reference the BusinessInvitation.create factory and the builder-based
construction to locate where to add these checks.
- Around line 80-82: The expire() method in BusinessInvitation currently
overwrites any status; add a transition guard so it only sets status to
BusinessInvitationStatus.EXPIRED when the current status is
BusinessInvitationStatus.PENDING (otherwise leave unchanged or throw an
IllegalStateException per domain rules). Update the BusinessInvitation.expire()
implementation to check the current status before assignment (referencing
BusinessInvitationStatus.PENDING and BusinessInvitationStatus.EXPIRED) to
enforce the invariant that only pending invitations may transition to expired.
- Around line 8-23: The BusinessInvitation domain class currently contains
framework annotations and imports (jakarta.persistence.*, lombok annotations,
org.springframework.data.* and `@Entity`, `@Table`, `@Builder`, `@NoArgsConstructor`,
`@AllArgsConstructor`, `@EntityListeners`) which couples the domain to JPA/Spring;
remove all these imports and annotations from the BusinessInvitation class to
make it a pure domain model (plain POJO/value object) and create a separate
persistence adapter (e.g., BusinessInvitationEntity or
BusinessInvitationJpaEntity) in the outbound/persistence layer that contains the
JPA annotations, auditing annotations, and mapping logic (mapper between
BusinessInvitation and BusinessInvitationEntity) so ORM concerns are isolated
from the domain.
In
`@src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessJoinRequest.java`:
- Around line 7-12: The BusinessJoinRequest class currently depends on
JPA/Spring Data; remove all infrastructure imports and annotations
(jakarta.persistence.*, org.springframework.data.annotation.*,
org.springframework.data.jpa.domain.support.AuditingEntityListener and
annotations: `@Entity`, `@Table`, `@Id`, `@GeneratedValue`, `@Column`, `@ManyToOne`,
`@JoinColumn`, `@CreatedDate`, `@LastModifiedDate`, `@EntityListeners`) so the domain
model is pure; create a separate persistence representation (e.g.,
BusinessJoinRequestEntity) in the infra layer that contains those JPA
annotations and maps fields to the domain BusinessJoinRequest, and add
conversion/mapping methods in the repository/adapter to translate between
BusinessJoinRequest and BusinessJoinRequestEntity (preserve fields like id,
relationships, created/updated timestamps in the entity).
- Around line 17-20: 유니크 제약 (workspace_id, user_id) 가 BusinessJoinRequest 엔티티의
현재 흐름과 충돌해 REJECTED/APPROVED 상태의 기존 레코드가 있어도 재요청 시 insert가 DB 무결성 예외로 실패합니다; 수정
방법은 테이블 제약을 변경해 중복 금지를 PENDING 상태에만 적용하도록 바꾸는 것으로, BusinessJoinRequest 엔티티의
`@Table`(uniqueConstraints=...) 설정을 제거하고 대신 DB 수준에서 partial unique index 또는 제약을
만들어 (workspace_id, user_id) 에 대해 WHERE status = 'PENDING' 조건을 적용하거나, 대체로 간단한
변경으로는 엔티티의 유니크 제약을 삭제하고 애플리케이션 레이어(예: 관련 Repository/Service의 createJoinRequest
또는 findExistingPendingRequest 메서드)에서 PENDING 중복을 확인한 뒤 insert 하도록 구현하세요.
In
`@src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/GetMyInvitationListUseCase.java`:
- Line 3: GetMyInvitationListUseCase currently imports MyInvitationResponseDto
from the adapter layer, creating an infrastructure dependency; change the
inbound port signature to use a domain-level DTO or interface (e.g., create
MyInvitationInfo or MyInvitationResponse in the domain layer) and replace
references to
com.dreamteam.alter.adapter.inbound.general.workspace.dto.MyInvitationResponseDto
with that new domain type in GetMyInvitationListUseCase and related inbound
ports; ensure adapters map/translate their adapter DTOs to the new domain DTO at
the adapter boundary so the domain layer has zero infrastructure dependencies.
In
`@src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/GetMyJoinRequestListUseCase.java`:
- Line 3: GetMyJoinRequestListUseCase currently imports adapter DTO
MyJoinRequestResponseDto, violating domain->adapter dependency rules; remove
that import and change the port to return a pure-domain type (e.g., create a
domain value/object named MyJoinRequest or MyJoinRequestModel in the domain
package) or move the DTO into the domain package, then update the
GetMyJoinRequestListUseCase method signature to use that domain type; keep all
mapping from domain -> MyJoinRequestResponseDto inside the adapter layer
(controller/mapper) so the domain layer has zero adapter/infrastructure
dependencies.
In
`@src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/GetWorkspaceJoinRequestListUseCase.java`:
- Around line 3-9: The domain port GetWorkspaceJoinRequestListUseCase is
depending on an adapter DTO (WorkspaceJoinRequestResponseDto); change the port
to return a pure domain type instead: remove the import of
WorkspaceJoinRequestResponseDto from the interface and replace the method
signature in GetWorkspaceJoinRequestListUseCase to return a domain model (e.g.,
List<WorkspaceJoinRequest> or a new WorkspaceJoinRequestDomainDto) while keeping
ManagerActor; then update the adapter/mapper layer to convert that domain model
to WorkspaceJoinRequestResponseDto (so WorkspaceJoinRequestResponseDto stays in
the adapter package and mapping is done in the inbound adapter/controller).
In
`@src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/SendWorkspaceInvitationUseCase.java`:
- Line 3: The interface SendWorkspaceInvitationUseCase currently imports and
depends on SendWorkspaceInvitationResultDto from the adapter layer, violating
the domain-layer ZERO-infrastructure rule; to fix it, remove the adapter DTO
import and either move SendWorkspaceInvitationResultDto into the domain module
or define a new domain-level result type (e.g., SendWorkspaceInvitationResult or
WorkspaceInvitationResult) and update the SendWorkspaceInvitationUseCase
signature and any callers to use that domain type instead of the adapter DTO;
ensure adapters map their own DTOs to/from this new domain result when crossing
the boundary.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 5f469e27-2c0c-4c7b-962f-362d11b4cf62
📒 Files selected for processing (43)
.gitignoresrc/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/controller/UserWorkspaceInvitationController.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/controller/UserWorkspaceInvitationControllerSpec.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/MyInvitationResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/MyJoinRequestResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/SendWorkspaceInvitationRequestDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceInvitationController.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceInvitationControllerSpec.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/SendWorkspaceInvitationResultDto.javasrc/main/java/com/dreamteam/alter/adapter/inbound/manager/workspace/dto/WorkspaceJoinRequestResponseDto.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessInvitationJpaRepository.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessInvitationQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessInvitationRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessJoinRequestJpaRepository.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessJoinRequestQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessJoinRequestRepositoryImpl.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/AcceptWorkspaceInvitation.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/ApproveJoinRequest.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/DeclineWorkspaceInvitation.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/GetMyInvitationList.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/GetMyJoinRequestList.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/GetWorkspaceJoinRequestList.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/RejectJoinRequest.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/SendJoinRequest.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.javasrc/main/java/com/dreamteam/alter/common/notification/NotificationMessageConstants.javasrc/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessInvitation.javasrc/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessJoinRequest.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/AcceptWorkspaceInvitationUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/ApproveJoinRequestUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/DeclineWorkspaceInvitationUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/GetMyInvitationListUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/GetMyJoinRequestListUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/GetWorkspaceJoinRequestListUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/RejectJoinRequestUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/SendJoinRequestUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/inbound/SendWorkspaceInvitationUseCase.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/outbound/BusinessInvitationQueryRepository.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/outbound/BusinessInvitationRepository.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/outbound/BusinessJoinRequestQueryRepository.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/outbound/BusinessJoinRequestRepository.javasrc/main/java/com/dreamteam/alter/domain/workspace/type/BusinessInvitationStatus.javasrc/main/java/com/dreamteam/alter/domain/workspace/type/BusinessJoinRequestStatus.java
...am/alter/adapter/inbound/general/workspace/controller/UserWorkspaceInvitationController.java
Show resolved
Hide resolved
...dreamteam/alter/adapter/inbound/general/workspace/dto/SendWorkspaceInvitationRequestDto.java
Outdated
Show resolved
Hide resolved
...alter/adapter/inbound/manager/workspace/controller/ManagerWorkspaceInvitationController.java
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/application/workspace/usecase/RejectJoinRequest.java
Outdated
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/application/workspace/usecase/SendJoinRequest.java
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessJoinRequest.java
Outdated
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/domain/workspace/port/inbound/GetMyInvitationListUseCase.java
Show resolved
Hide resolved
...main/java/com/dreamteam/alter/domain/workspace/port/inbound/GetMyJoinRequestListUseCase.java
Show resolved
Hide resolved
...va/com/dreamteam/alter/domain/workspace/port/inbound/GetWorkspaceJoinRequestListUseCase.java
Outdated
Show resolved
Hide resolved
...n/java/com/dreamteam/alter/domain/workspace/port/inbound/SendWorkspaceInvitationUseCase.java
Outdated
Show resolved
Hide resolved
There was a problem hiding this comment.
♻️ Duplicate comments (1)
src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/SendWorkspaceInvitationRequestDto.java (1)
15-16:⚠️ Potential issue | 🟠 Major전화번호 검증이 길이 기준만으로는 부족합니다.
Line 16은
@NotBlank/@Size만 사용해"abcdefghij"같은 값도 통과합니다. 전화번호 형식(숫자/패턴) 제약을 요소 단위로 추가해 주세요.✅ 제안 수정안
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.Pattern; import jakarta.validation.constraints.Size; @@ `@NotEmpty` - private List<@NotBlank `@Size`(min = 10, max = 11) String> phoneNumbers; + private List< + `@NotBlank` + `@Size`(min = 10, max = 11) + `@Pattern`(regexp = "^010\\d{8}$", message = "올바른 전화번호 형식이 아닙니다") + String + > phoneNumbers; }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/SendWorkspaceInvitationRequestDto.java` around lines 15 - 16, The phoneNumbers field in SendWorkspaceInvitationRequestDto currently only uses `@NotBlank` and `@Size` so non-numeric strings pass validation; update the element-level constraints to enforce a phone-number pattern by adding a `@Pattern` on the list element (e.g., List<@Pattern(regexp="^(?:\\+?\\d{1,3}[- ]?)?\\d{10,11}$") `@NotBlank` `@Size`(min = 10, max = 11) String> phoneNumbers) to ensure digits and optional country code/formatting, and add the corresponding import for javax.validation.constraints.Pattern; keep the existing `@NotBlank/`@Size alongside the `@Pattern` on the phoneNumbers field.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Duplicate comments:
In
`@src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/SendWorkspaceInvitationRequestDto.java`:
- Around line 15-16: The phoneNumbers field in SendWorkspaceInvitationRequestDto
currently only uses `@NotBlank` and `@Size` so non-numeric strings pass validation;
update the element-level constraints to enforce a phone-number pattern by adding
a `@Pattern` on the list element (e.g., List<@Pattern(regexp="^(?:\\+?\\d{1,3}[-
]?)?\\d{10,11}$") `@NotBlank` `@Size`(min = 10, max = 11) String> phoneNumbers) to
ensure digits and optional country code/formatting, and add the corresponding
import for javax.validation.constraints.Pattern; keep the existing
`@NotBlank/`@Size alongside the `@Pattern` on the phoneNumbers field.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 4b437d0c-5752-4b70-80d2-ef71935543f3
📒 Files selected for processing (1)
src/main/java/com/dreamteam/alter/adapter/inbound/general/workspace/dto/SendWorkspaceInvitationRequestDto.java
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessJoinRequest.java (1)
7-21:⚠️ Potential issue | 🟠 Major도메인 엔티티에 인프라 의존성이 직접 포함되어 있습니다.
JPA/Spring Data 어노테이션과 import가 도메인 레이어에 남아 있어 계층 경계가 깨집니다. 도메인 모델과 영속성 모델을 분리해 주세요.
As per coding guidelines, "ZERO infrastructure dependencies (no Spring, no JPA annotations, no external libs)."
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessJoinRequest.java` around lines 7 - 21, The BusinessJoinRequest class currently contains JPA/Spring Data annotations and imports (e.g., `@Entity`, `@Table`, `@EntityListeners`(AuditingEntityListener.class), `@CreatedDate`, `@LastModifiedDate` and their jakarta/spring imports); extract a pure domain model (BusinessJoinRequest) with no framework imports or annotations (only plain fields, constructors/getters and business logic) and move all persistence concerns into a separate persistence-mapped class (e.g., BusinessJoinRequestEntity) in the infra/persistence layer that keeps the JPA annotations and imports and maps to/from the domain model via a mapper or constructor; remove all JPA/Spring imports from the domain file and update repositories/services to use the mapper to convert between BusinessJoinRequest and BusinessJoinRequestEntity.
🤖 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/dreamteam/alter/domain/workspace/entity/BusinessJoinRequest.java`:
- Around line 48-53: The static factory BusinessJoinRequest.create allows null
workspace or user which violates domain invariants; update
BusinessJoinRequest.create to validate and reject nulls for workspace and user
(e.g., using Objects.requireNonNull or explicit checks) before calling
BusinessJoinRequest.builder() so that Workspace and User cannot be null on
creation and the status remains BusinessJoinRequestStatus.PENDING; throw a clear
runtime exception (IllegalArgumentException or NullPointerException) when
validation fails to fail fast.
---
Duplicate comments:
In
`@src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessJoinRequest.java`:
- Around line 7-21: The BusinessJoinRequest class currently contains JPA/Spring
Data annotations and imports (e.g., `@Entity`, `@Table`,
`@EntityListeners`(AuditingEntityListener.class), `@CreatedDate`, `@LastModifiedDate`
and their jakarta/spring imports); extract a pure domain model
(BusinessJoinRequest) with no framework imports or annotations (only plain
fields, constructors/getters and business logic) and move all persistence
concerns into a separate persistence-mapped class (e.g.,
BusinessJoinRequestEntity) in the infra/persistence layer that keeps the JPA
annotations and imports and maps to/from the domain model via a mapper or
constructor; remove all JPA/Spring imports from the domain file and update
repositories/services to use the mapper to convert between BusinessJoinRequest
and BusinessJoinRequestEntity.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: f9e36ad0-15a9-485d-8048-b2991499e6b2
📒 Files selected for processing (2)
src/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessJoinRequestRepositoryImpl.javasrc/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessJoinRequest.java
src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessJoinRequest.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 5
♻️ Duplicate comments (2)
src/main/java/com/dreamteam/alter/application/workspace/usecase/SendJoinRequest.java (1)
42-47:⚠️ Potential issue | 🟠 Major중복 합류 요청 방지 로직이 TOCTOU 경쟁 조건에 취약합니다.
existsPendingRequest확인(Line 42)과save(Line 47)가 분리되어 있어 동시 요청 시 중복 PENDING 요청이 생성될 수 있습니다. DB 레벨의 유니크 제약조건(예: workspace_id + user_id + status=PENDING)과DataIntegrityViolationException처리를 통해 원자성을 확보해야 합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/application/workspace/usecase/SendJoinRequest.java` around lines 42 - 47, The existsPendingRequest check before calling businessJoinRequestRepository.save is vulnerable to TOCTOU races; add a DB unique constraint (workspace_id + user_id + status=PENDING) on the BusinessJoinRequest table and remove reliance on the in-memory pre-check as the sole guard, then wrap the save call (BusinessJoinRequest.create(...) followed by businessJoinRequestRepository.save(...)) in a try/catch that catches DataIntegrityViolationException (or the specific constraint violation) and translates it to the existing CustomException(ErrorCode.CONFLICT, ...) message; keep the create/save flow and change only error handling so concurrent inserts are handled atomically by the DB.src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java (1)
69-76:⚠️ Potential issue | 🟠 Major중복 초대 방지 로직이 TOCTOU 경쟁 조건에 취약합니다.
existsPendingInvitation확인(Line 69)과save(Line 74-76)가 분리되어 있어 동시 요청 시 중복 PENDING 초대가 생성될 수 있습니다. DB 레벨의 유니크 제약조건과DataIntegrityViolationException처리를 권장합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java` around lines 69 - 76, The existsPendingInvitation check in businessInvitationQueryRepository and the subsequent businessInvitationRepository.save(BusinessInvitation.create(...)) are vulnerable to TOCTOU races; add a DB-level UNIQUE constraint (e.g., on workspace_id + invited_user_id + status=PENDING or equivalent) and wrap the save call in a try/catch that handles DataIntegrityViolationException (or the JPA/SQL equivalent) to treat unique-constraint violations as "already invited" (add the phone number to alreadyInvitedPhoneNumbers) instead of failing; keep the initial existsPendingInvitation fast-path for common cases but rely on the DB constraint+exception handling around businessInvitationRepository.save to guarantee uniqueness under concurrent requests.
🤖 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/dreamteam/alter/application/notification/FcmNotificationEvent.java`:
- Around line 3-5: FcmNotificationEvent currently depends on adapter-layer DTO
FcmNotificationRequestDto; change this by introducing an
application-layer/internal payload (e.g., FcmNotificationPayload or move the DTO
to application.notification or domain) and update the record signature
FcmNotificationEvent(...) to accept that internal type instead of
com.dreamteam.alter.adapter.inbound.common.dto.FcmNotificationRequestDto; adjust
all creators/consumers of FcmNotificationEvent to map from the inbound DTO to
the new application payload at the adapter boundary so the application layer no
longer imports adapter.inbound.
In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/ApproveJoinRequest.java`:
- Around line 31-32: Replace the field-level `@Resource` injection of
CreateWorkspaceWorkerUseCase (addWorkerToWorkspace) with constructor injection
to match `@RequiredArgsConstructor`; make the field final and remove `@Resource` so
Lombok generates a constructor-based injection, or if `@Resource` was intentional
(e.g., to avoid a circular dependency), leave it but add a clear comment above
addWorkerToWorkspace explaining that reason.
- Around line 22-25: Add the Lombok logging annotation to the ApproveJoinRequest
use-case class: annotate the class ApproveJoinRequest with `@Slf4j` (matching the
pattern used in RejectJoinRequest) and ensure the lombok.extern.slf4j.Slf4j
import is present so you can use log within methods for consistent logging.
In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/RejectJoinRequest.java`:
- Around line 20-23: Add the missing Lombok logging annotation to the use case
class: annotate the RejectJoinRequest class with `@Slf4j` and import
lombok.extern.slf4j.Slf4j so logging is available (matching the pattern used in
SendJoinRequest and SendWorkspaceInvitation); ensure the annotation is placed
above the class declaration alongside `@Service` and `@RequiredArgsConstructor`.
In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java`:
- Around line 55-80: The loop triggers N+1 queries; replace per-phone queries
with batched lookups: call userQueryRepository.findByContactIn(phoneNumbers) and
build a map from contact->User, call
workspaceQueryRepository.findActiveWorkerUserIds(workspaceId) to get a Set of
active user ids, and call
businessInvitationQueryRepository.findPendingInvitedUserIds(workspace) to get a
Set of already-invited user ids; then iterate the original phoneNumbers, use the
maps/sets to classify numbers (unregistered, alreadyWorker, alreadyInvited),
create BusinessInvitation instances via BusinessInvitation.create(workspace,
invitedUser, actor.getManagerUser()) and persist them in bulk (e.g.,
businessInvitationRepository.saveAll or similar), update successCount
accordingly, and sendInvitationNotification for each newly-created invitation
(or batch notifications) instead of doing individual repository calls inside the
loop.
---
Duplicate comments:
In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/SendJoinRequest.java`:
- Around line 42-47: The existsPendingRequest check before calling
businessJoinRequestRepository.save is vulnerable to TOCTOU races; add a DB
unique constraint (workspace_id + user_id + status=PENDING) on the
BusinessJoinRequest table and remove reliance on the in-memory pre-check as the
sole guard, then wrap the save call (BusinessJoinRequest.create(...) followed by
businessJoinRequestRepository.save(...)) in a try/catch that catches
DataIntegrityViolationException (or the specific constraint violation) and
translates it to the existing CustomException(ErrorCode.CONFLICT, ...) message;
keep the create/save flow and change only error handling so concurrent inserts
are handled atomically by the DB.
In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java`:
- Around line 69-76: The existsPendingInvitation check in
businessInvitationQueryRepository and the subsequent
businessInvitationRepository.save(BusinessInvitation.create(...)) are vulnerable
to TOCTOU races; add a DB-level UNIQUE constraint (e.g., on workspace_id +
invited_user_id + status=PENDING or equivalent) and wrap the save call in a
try/catch that handles DataIntegrityViolationException (or the JPA/SQL
equivalent) to treat unique-constraint violations as "already invited" (add the
phone number to alreadyInvitedPhoneNumbers) instead of failing; keep the initial
existsPendingInvitation fast-path for common cases but rely on the DB
constraint+exception handling around businessInvitationRepository.save to
guarantee uniqueness under concurrent requests.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 2b31083e-d279-412f-9133-e65e26345cc0
📒 Files selected for processing (6)
src/main/java/com/dreamteam/alter/application/notification/FcmNotificationEvent.javasrc/main/java/com/dreamteam/alter/application/notification/FcmNotificationEventListener.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/ApproveJoinRequest.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/RejectJoinRequest.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/SendJoinRequest.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java
src/main/java/com/dreamteam/alter/application/notification/FcmNotificationEvent.java
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/application/workspace/usecase/ApproveJoinRequest.java
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/application/workspace/usecase/ApproveJoinRequest.java
Outdated
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/application/workspace/usecase/RejectJoinRequest.java
Show resolved
Hide resolved
There was a problem hiding this comment.
Actionable comments posted: 2
♻️ Duplicate comments (1)
src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java (1)
57-83:⚠️ Potential issue | 🔴 Critical중복 초대 방지가 원자적으로 보장되지 않습니다.
Line 57에서 기존 PENDING 초대를 조회하고 Line 83에서 저장하는 흐름은 동시 요청 시 TOCTOU 경쟁 조건이 남아 있습니다. 결과적으로 동일
(workspace, invitedUser, PENDING)초대가 중복 생성될 수 있습니다. DB 유니크 제약(부분 유니크 포함)과 저장 충돌 처리(이미 초대됨으로 분류)가 필요합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java` around lines 57 - 83, The current TOCTOU risk comes from reading pending invitations via businessInvitationQueryRepository.findPendingInvitedUserIds and later calling businessInvitationRepository.saveAll(BusinessInvitation.create(...)), which can produce duplicate PENDING invitations under concurrent requests; add a DB-level uniqueness constraint (partial unique index on workspace_id + invited_user_id where status='PENDING') and make the use case resilient by catching the persistence/constraint exception (e.g., DataIntegrityViolationException or the DB-specific unique constraint exception) around businessInvitationRepository.saveAll; on catch, translate the conflict into already-invited handling (add affected phone numbers to alreadyInvitedPhoneNumbers) and/or re-query pendingInvitedUserIds to update state instead of failing the whole request. Ensure references: businessInvitationQueryRepository.findPendingInvitedUserIds, businessInvitationRepository.saveAll, BusinessInvitation.create, pendingInvitedUserIds, alreadyInvitedPhoneNumbers, invitedUser, workspace, actor.getManagerUser.
🤖 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/dreamteam/alter/adapter/outbound/user/persistence/UserQueryRepositoryImpl.java`:
- Around line 121-128: findByContactIn 메서드의 contacts 파라미터에 대한 null/빈 검사 로직이 없어서
qUser.contact.in(contacts)에서 NPE가 발생할 수 있습니다;
UserQueryRepositoryImpl.findByContactIn 시작부에 contacts가 null 이거나 비어있으면 빈
List<User>를 즉시 반환하도록 방어 코드를 추가하고, 그렇지 않으면 기존
queryFactory.selectFrom(qUser).where(qUser.contact.in(contacts),
qUser.status.eq(UserStatus.ACTIVE)).fetch() 로직을 실행하도록 변경하세요
(WorkspaceWorkerQueryRepositoryImpl.findAllById의 패턴과 일치시켜 처리).
In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java`:
- Around line 64-81: The loop can add duplicate invitations for the same
phone/user because it never records newly added invites during the same request;
create a local Set (e.g., newlyInvitedUserIds or newlyInvitedPhoneNumbers) and
check it alongside pendingInvitedUserIds/activeWorkerUserIds before creating an
invitation, and after invitationsToSave.add(BusinessInvitation.create(...)) add
the invited user's id (or phone) into that set so subsequent iterations skip
duplicates; reference phoneNumbers, contactToUser, invitedUser.getId(),
pendingInvitedUserIds, invitationsToSave and BusinessInvitation.create to locate
and update the loop.
---
Duplicate comments:
In
`@src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java`:
- Around line 57-83: The current TOCTOU risk comes from reading pending
invitations via businessInvitationQueryRepository.findPendingInvitedUserIds and
later calling
businessInvitationRepository.saveAll(BusinessInvitation.create(...)), which can
produce duplicate PENDING invitations under concurrent requests; add a DB-level
uniqueness constraint (partial unique index on workspace_id + invited_user_id
where status='PENDING') and make the use case resilient by catching the
persistence/constraint exception (e.g., DataIntegrityViolationException or the
DB-specific unique constraint exception) around
businessInvitationRepository.saveAll; on catch, translate the conflict into
already-invited handling (add affected phone numbers to
alreadyInvitedPhoneNumbers) and/or re-query pendingInvitedUserIds to update
state instead of failing the whole request. Ensure references:
businessInvitationQueryRepository.findPendingInvitedUserIds,
businessInvitationRepository.saveAll, BusinessInvitation.create,
pendingInvitedUserIds, alreadyInvitedPhoneNumbers, invitedUser, workspace,
actor.getManagerUser.
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: ASSERTIVE
Plan: Pro
Run ID: 3fa1dc2a-98d0-4ea3-959a-9932280c5945
📒 Files selected for processing (9)
src/main/java/com/dreamteam/alter/adapter/outbound/user/persistence/UserQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessInvitationQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/BusinessInvitationRepositoryImpl.javasrc/main/java/com/dreamteam/alter/adapter/outbound/workspace/persistence/WorkspaceQueryRepositoryImpl.javasrc/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.javasrc/main/java/com/dreamteam/alter/domain/user/port/outbound/UserQueryRepository.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/outbound/BusinessInvitationQueryRepository.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/outbound/BusinessInvitationRepository.javasrc/main/java/com/dreamteam/alter/domain/workspace/port/outbound/WorkspaceQueryRepository.java
...main/java/com/dreamteam/alter/adapter/outbound/user/persistence/UserQueryRepositoryImpl.java
Outdated
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java
Show resolved
Hide resolved
ysw789
left a comment
There was a problem hiding this comment.
- 기본적으로 모든 목록 조회 UseCase에는 필터 기반 조건 조회와 Cursor Pagination을 구성해주세요
Controller->UseCase로 인자 전달 시 RequestBody를 매핑한 DTO에서 값을 꺼내지 말고 그대로 전달하게 해주세요- 응답/요청 DTO에 API 스펙 추가해주세요
- 응답 DTO 의 인스턴스 생성 시에는 기본 생성자의 접근제한자를 PRIVATE으로 하고 외부에서는 팩토리 메소드를 통해서만 접근 가능하도록 하고 있습니다. 관련 패턴은 사전에 구현되어있는 응답 DTO들을 참조
src/main/java/com/dreamteam/alter/domain/workspace/entity/BusinessInvitation.java
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/application/workspace/usecase/GetMyInvitationList.java
Outdated
Show resolved
Hide resolved
...main/java/com/dreamteam/alter/application/workspace/usecase/GetWorkspaceJoinRequestList.java
Outdated
Show resolved
Hide resolved
...dreamteam/alter/adapter/inbound/manager/workspace/dto/SendWorkspaceInvitationRequestDto.java
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java
Show resolved
Hide resolved
src/main/java/com/dreamteam/alter/application/workspace/usecase/SendWorkspaceInvitation.java
Outdated
Show resolved
Hide resolved
ysw789
left a comment
There was a problem hiding this comment.
GetMyInvitationList, GetMyJoinRequestList, GetWorkspaceJoinRequestList 공통
- 컨트롤러에서 필터링 파라미터는 ModelAttribute 통해서 DTO로 받아오게 수정해주세요. 이렇게 수정하면 쿼리에 필터링 인자를 전달할 때도 DTO 그대로 넘길 수 있습니다
- UseCase에서 커서 정보 생성 로직은 기존에 있는 패턴 참조해서 맞춰주세요. 지금 구현으로는 조건에 해당하는 전체 크기 (totalCount)를 알 수가 없습니다.
- 조회 쿼리에서 필터 파라미터의 null 여부 확인은 별도의 메소드로 빼두면 count 확인과 실제 값 조회용 메소드 두 곳에서 같이 사용 가능합니다
관련 문서
https://www.notion.so/BE-3188655316288049bfe4d24d8503ff03?source=copy_link
Summary by CodeRabbit
New Features
Chores