Skip to content

Conversation

@ehddms0320
Copy link

@ehddms0320 ehddms0320 commented May 30, 2025

springdoc-openapi-starter-webmvc-ui 적용, 모집글 Controller 기반 Swagger UI 확인 완료

Summary by CodeRabbit

  • 신규 기능

    • 오픈API(Swagger) UI 연동 및 테스트 엔드포인트(/swagger-test) 추가.
    • 주문 요청 및 응답용 DTO(OrderRequestDto, OrderResponseDto) 도입.
    • 주문 서버와 연동하는 OrderClient 및 관련 서비스(RecruitmentService) 추가.
    • 모집글 생성 시 카테고리 및 메뉴 정보 입력 가능, 주문 생성과 연동.
    • 모집 참여 시 주문 정보 전송 및 참여자별 주문 ID 저장.
  • 기능 개선

    • 모집글 관련 엔드포인트 경로가 /api/v1/recruitments로 변경.
    • 모집글 생성·참여·상세조회 등 주요 로직이 서비스 계층으로 이전되어 코드 구조 개선.
    • 예외 처리 시 Swagger 관련 요청은 기본 처리로 우회.
  • 환경 설정

    • 주문 서버 연동을 위한 외부 API URL 및 경로 환경 변수 추가.
  • 버그 수정 및 기타

    • 일부 필드 및 메서드 추가, 불필요한 코드 및 주석 정리.

@ehddms0320 ehddms0320 temporarily deployed to recruitment-service May 30, 2025 14:20 — with GitHub Actions Inactive
@coderabbitai
Copy link

coderabbitai bot commented May 30, 2025

Walkthrough

이번 변경에서는 주문(Order) 서버와의 연동을 위한 클라이언트 및 DTO가 추가되고, 리크루트(Recruitment) 생성 및 참여 로직이 서비스 계층으로 이동되었습니다. Swagger(OpenAPI) UI 연동을 위한 의존성 및 테스트 컨트롤러가 추가되었으며, 관련 엔티티와 예외 처리, 컨트롤러, 레포지토리에도 필드 및 메서드가 확장되었습니다.

Changes

파일/경로 그룹 변경 요약
recruitment-service/build.gradle, src/main/resources/application.properties Spring Boot/Dependency Management 버전 다운그레이드, OpenAPI UI 의존성 및 주문 서버 URL/경로 프로퍼티 추가
.../controller/SwaggerTestController.java Swagger 테스트용 REST 컨트롤러 신설
.../dto/order/OrderRequestDto.java, .../dto/order/OrderResponseDto.java 주문 요청/응답 DTO 클래스 신설, 메뉴 및 옵션 정보 포함
.../dto/RecruitmentRequestDto.java 카테고리 및 메뉴 필드, OrderRequestDto 변환 메서드 추가
.../entity/Recruitment.java 카테고리, orderId, orderIds 필드 및 orderId 추가 메서드 추가
.../entity/RecruitmentParticipant.java orderId 필드 추가
.../repository/RecruitmentParticipantRepository.java 엔티티 기반 참여 여부 확인 메서드 추가
.../service/OrderClient.java 주문 서버와 연동하는 클라이언트 서비스 신설
.../service/RecruitmentService.java 리크루트 생성/참여 로직을 담당하는 서비스 신설
.../controller/RecruitmentController.java 경로 변경(/api/v1), 비즈니스 로직 서비스 위임, 참여 엔드포인트 추가, 응답 형식 일부 변경
.../exception/GlobalExceptionHandler.java Swagger 관련 요청 시 예외 핸들러 우회 로직 추가 및 시그니처 변경

Sequence Diagram(s)

sequenceDiagram
actor User
participant RecruitmentController
participant RecruitmentService
participant OrderClient
participant RecruitmentRepository
participant UserRepository
participant StoreRepository
participant RecruitmentParticipantRepository
participant OrderServer

User->>RecruitmentController: POST /api/v1/recruitments
RecruitmentController->>RecruitmentService: createRecruitment(dto)
RecruitmentService->>UserRepository: findById(userId)
RecruitmentService->>StoreRepository: findById(storeId)
RecruitmentService->>RecruitmentRepository: save(Recruitment)
RecruitmentService->>OrderClient: createOrder(OrderRequestDto)
OrderClient->>OrderServer: POST /order (OrderRequestDto)
OrderServer-->>OrderClient: { orderId }
OrderClient-->>RecruitmentService: orderId
RecruitmentService->>RecruitmentRepository: save(orderId)
RecruitmentService-->>RecruitmentController: 완료

User->>RecruitmentController: POST /api/v1/recruitments/{id}/join
RecruitmentController->>RecruitmentService: joinRecruitment(id, userId, orderDto)
RecruitmentService->>RecruitmentRepository: findById(id)
RecruitmentService->>UserRepository: findById(userId)
RecruitmentService->>RecruitmentParticipantRepository: existsByRecruitmentAndUser
RecruitmentService->>OrderClient: createOrder(orderDto)
OrderClient->>OrderServer: POST /order (OrderRequestDto)
OrderServer-->>OrderClient: { orderId }
OrderClient-->>RecruitmentService: orderId
RecruitmentService->>RecruitmentParticipantRepository: save(RecruitmentParticipant)
RecruitmentService-->>RecruitmentController: 완료
Loading

Suggested labels

enhancement

Poem

🐰
새로운 주문과 리크루트, 토끼도 신이 나,
서비스로 분리된 로직, 깔끔하게 날아가~
Swagger 문서도 활짝, API가 반짝반짝!
메뉴와 옵션까지 담아,
오늘도 깡총깡총,
코드밭을 누비는 토끼는
기쁨에 귀가 쫑긋!

✨ Finishing Touches
  • 📝 Generate Docstrings

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
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Explain this complex logic.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

CodeRabbit Configuration File (.coderabbit.yaml)

  • You can programmatically configure CodeRabbit by adding a .coderabbit.yaml file to the root of your repository.
  • Please see the configuration documentation for more information.
  • If your editor has YAML language server enabled, you can add the path at the top of this file to enable auto-completion and validation: # yaml-language-server: $schema=https://coderabbit.ai/integrations/schema.v2.json

Documentation and Community

  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link

@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: 5

🧹 Nitpick comments (6)
recruitment-service/src/main/java/com/example/recruitment/repository/OrderRepository.java (1)

1-7: 표준 JPA 저장소 패턴이 올바르게 구현되었습니다.

기본 CRUD 및 페이징 작업을 위한 표준 Spring Data JPA 저장소 인터페이스가 정확하게 구현되었습니다. Order 엔티티와의 연동도 적절합니다.

향후 주문 관련 비즈니스 로직이 복잡해질 경우, 커스텀 쿼리 메소드 추가를 고려해보세요 (예: findByRecruitmentId, findByUserId 등).

recruitment-service/src/main/java/com/example/recruitment/entity/Recruitment.java (1)

34-34: 주석의 오타를 수정해주세요.

주석에 오타가 있습니다. "카테고리 추가가" → "카테고리 추가"로 수정해주세요.

-    //카테고리 추가가
+    //카테고리 추가
recruitment-service/src/main/java/com/example/recruitment/entity/MenuItem.java (1)

6-14: 임베디드 엔티티 구조가 적절하게 구현되었습니다.

@Embeddable 어노테이션과 Lombok을 사용한 구현이 깔끔합니다. 주문 내 메뉴 항목을 표현하기에 적절한 설계입니다.

향후 개선을 위한 제안사항:

  1. 가격 필드 개선: 통화 처리의 정확성을 위해 BigDecimal 사용을 권장합니다.
-    private int price;
+    private BigDecimal price;
  1. 검증 어노테이션 추가: 데이터 무결성을 위한 검증 추가를 고려해보세요.
@NotBlank
private String name;

@DecimalMin(value = "0.0", inclusive = false)
private BigDecimal price;
recruitment-service/src/main/java/com/example/recruitment/entity/Order.java (1)

24-26: ElementCollection의 성능 영향을 검토해주세요.

@ElementCollection은 별도 테이블을 생성하고 항상 eager loading됩니다. 메뉴 아이템이 많아질 경우 성능 문제가 발생할 수 있습니다.

대안으로 별도 엔티티로 분리하는 것을 고려해보세요:

-    @ElementCollection
-    @CollectionTable(name = "order_menu_items", joinColumns = @JoinColumn(name = "order_id"))
-    private List<MenuItem> menuItems; // 메뉴 리스트 (내장 객체로 구성)
+    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL, fetch = FetchType.LAZY)
+    private List<OrderMenuItem> orderMenuItems; // 메뉴 리스트
recruitment-service/src/main/java/com/example/recruitment/controller/SwaggerTestController.java (1)

7-14: 테스트 컨트롤러의 운영 환경 사용에 대해 고려해주세요.

Swagger UI 테스트를 위한 컨트롤러가 적절하게 구현되었습니다. 그러나 이러한 테스트 전용 컨트롤러가 운영 환경에 배포되는 것이 적절한지 검토해주세요.

운영 환경에서는 이런 테스트 엔드포인트를 제거하는 것을 고려해보세요:

+@Profile("!prod")  // 운영 환경에서는 비활성화
@RestController
@RequestMapping("/swagger-test")
public class SwaggerTestController {

또는 별도의 설정을 통해 조건부로 활성화하는 방안도 있습니다.

recruitment-service/src/main/java/com/example/recruitment/exception/GlobalExceptionHandler.java (1)

46-47: 불필요한 빈 줄을 제거해주세요.

코드 가독성을 위해 불필요한 빈 줄들을 제거하는 것이 좋겠습니다.

         }
-
-
-
         return ResponseEntity
📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 45de156 and d20dc23.

📒 Files selected for processing (8)
  • recruitment-service/build.gradle (2 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/controller/SwaggerTestController.java (1 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/dto/RecruitmentRequestDto.java (1 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/entity/MenuItem.java (1 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/entity/Order.java (1 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/entity/Recruitment.java (1 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/exception/GlobalExceptionHandler.java (3 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/repository/OrderRepository.java (1 hunks)
🔇 Additional comments (6)
recruitment-service/src/main/java/com/example/recruitment/entity/Recruitment.java (1)

35-36: 카테고리 필드가 적절하게 구현되었습니다.

@Column(nullable = false) 어노테이션으로 데이터베이스 레벨에서 필수값을 보장하는 것이 좋습니다. DTO의 @NotBlank 검증과도 일치합니다.

recruitment-service/src/main/java/com/example/recruitment/dto/RecruitmentRequestDto.java (1)

31-32: 카테고리 필드가 일관성 있게 추가되었습니다.

엔티티의 category 필드와 일치하며, @NotBlank 검증과 한국어 오류 메시지가 다른 필드들과 일관성을 유지하고 있습니다. 데이터 검증이 적절하게 구현되었습니다.

recruitment-service/src/main/java/com/example/recruitment/entity/Order.java (1)

24-26:

Details

✅ Verification successful

MenuItem 클래스의 정의를 확인해주세요.

MenuItem 클래스가 @Embeddable로 정의되어 있는지 확인이 필요합니다.

MenuItem 클래스가 올바르게 정의되었는지 확인하기 위해 다음 스크립트를 실행해주세요:


🏁 Script executed:

#!/bin/bash
# MenuItem 클래스 정의 확인
ast-grep --pattern 'class MenuItem {
  $$$
}'

# @Embeddable 어노테이션 확인
rg -A 10 "@Embeddable.*MenuItem"

Length of output: 82


다음 스크립트로 MenuItem 클래스 정의와 @Embeddable 어노테이션 존재 여부를 재확인해주세요:


🏁 Script executed:

#!/bin/bash
# MenuItem.java 파일 경로 검색
file=$(fd -e java MenuItem.java)

echo ">> MenuItem.java 위치: $file"

# 클래스 선언 확인
rg -n "^\\s*(public\\s+)?class\\s+MenuItem" "$file"

# @Embeddable 어노테이션 확인 (클래스 바로 위 주석 포함 3줄)
rg -C3 "@Embeddable" "$file"

Length of output: 695


MenuItem 클래스에 @embeddable 어노테이션이 올바르게 적용되어 있습니다

recruitment-service/src/main/java/com/example/recruitment/entity/MenuItem.java 파일에서 @Embeddable이 확인되었으므로, @ElementCollection 매핑이 정상적으로 동작합니다. 추가 수정은 필요하지 않습니다.

recruitment-service/build.gradle (2)

31-31: OpenAPI 의존성 추가가 적절합니다.

Swagger UI 통합을 위한 springdoc-openapi-starter-webmvc-ui 의존성이 적절하게 추가되었습니다. 버전 2.2.0은 Spring Boot 3.x와 호환됩니다.


3-4:

Details

❓ Verification inconclusive

버전 다운그레이드에 대한 검증이 필요합니다.

Spring Boot와 dependency management 플러그인의 버전이 다운그레이드되었습니다. 이러한 변경사항이 필요한 이유와 보안 취약점 및 호환성 문제가 없는지 확인해주세요.

다음 스크립트를 실행하여 해당 버전들의 보안 취약점을 확인해보겠습니다:


🏁 Script executed:

#!/bin/bash
# Description: Check for security advisories for Spring Boot versions

# Check for Spring Boot 3.1.5 security issues
curl -s "https://api.github.com/repos/spring-projects/spring-boot/releases" | jq '[.[] | select(.tag_name | startswith("v3.1")) | {tag_name, published_at, body}] | .[0:5]'

# Check for Spring Boot 3.4.5 security issues  
curl -s "https://api.github.com/repos/spring-projects/spring-boot/releases" | jq '[.[] | select(.tag_name | startswith("v3.4")) | {tag_name, published_at, body}] | .[0:5]'

Length of output: 36620


버전 다운그레이드 보안·호환성 검증 필요

Spring Boot 및 의존성 관리 플러그인 버전이 하향 조정되어 보안 취약점 및 호환성 리스크가 발생할 수 있습니다. 공식 CVE/NVD 데이터베이스와 Spring Boot 3.1.5 릴리스 노트를 검토해 주세요.

· recruitment-service/build.gradle

  • org.springframework.boot 플러그인 3.4.5 → 3.1.5 (3행)
  • io.spring.dependency-management 플러그인 1.1.7 → 1.1.3 (4행)

· 보안·호환성 검증 권장

  • NVD/CVE 데이터베이스에서 Spring Boot 3.1.5 취약점 확인
  • Spring Boot 릴리스 노트(v3.1.5) 및 GitHub Releases(?page= 파라미터 활용) 검토
recruitment-service/src/main/java/com/example/recruitment/exception/GlobalExceptionHandler.java (1)

39-44: Swagger 요청 처리 로직이 적절하게 구현되었습니다.

Swagger 관련 요청에 대해 글로벌 예외 처리를 건너뛰는 로직이 올바르게 구현되었습니다. 이는 OpenAPI 문서화가 정상적으로 작동하도록 보장합니다.

다만 경로 검사 로직을 더욱 견고하게 만들 수 있습니다:

-        if (path != null && (path.contains("/v3/api-docs") || path.contains("/swagger"))) {
+        if (path != null && (path.startsWith("/v3/api-docs") || path.startsWith("/swagger-ui") || path.contains("/swagger"))) {
             throw e; // Swagger 요청은 예외 처리하지 않고 Spring에게 맡김
         }

private String title;
private String description;

private String status; // RECRUITING, CONFIRMED 등
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

상태 필드에 열거형(Enum) 사용을 권장합니다.

문자열 대신 열거형을 사용하여 타입 안전성을 향상시키고 유효하지 않은 상태 값 입력을 방지할 수 있습니다.

다음과 같이 열거형을 정의하고 사용하는 것을 권장합니다:

+@Enumerated(EnumType.STRING)
+private RecruitmentStatus status;  // RECRUITING, CONFIRMED 등
-private String status;  // RECRUITING, CONFIRMED 등

별도의 enum 클래스를 생성하세요:

public enum RecruitmentStatus {
    RECRUITING, CONFIRMED, CLOSED
}
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/entity/Recruitment.java
at line 30, replace the String type status field with an enum type to improve
type safety and prevent invalid values. Create a separate enum class named
RecruitmentStatus with constants RECRUITING, CONFIRMED, and CLOSED, then change
the status field's type to RecruitmentStatus and update any related code
accordingly.

Comment on lines 18 to 22
@ManyToOne
private Recruitment recruitment; // 어떤 모집글에 대한 주문인지

@ManyToOne
private User user; // 누가 주문했는지
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

관계 매핑에 fetch 타입과 cascade 설정을 추가해주세요.

@ManyToOne 관계에서 fetch 타입과 cascade 옵션을 명시적으로 설정하면 성능과 데이터 일관성을 보장할 수 있습니다.

-    @ManyToOne
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "recruitment_id", nullable = false)
     private Recruitment recruitment; // 어떤 모집글에 대한 주문인지
 
-    @ManyToOne
+    @ManyToOne(fetch = FetchType.LAZY)
+    @JoinColumn(name = "user_id", nullable = false)
     private User user; // 누가 주문했는지

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In recruitment-service/src/main/java/com/example/recruitment/entity/Order.java
around lines 18 to 22, the @ManyToOne annotations for recruitment and user
fields lack explicit fetch type and cascade settings. To improve performance and
data consistency, add fetch=FetchType.LAZY and appropriate cascade options
(e.g., CascadeType.PERSIST or CascadeType.MERGE) to both @ManyToOne annotations.

@CollectionTable(name = "order_menu_items", joinColumns = @JoinColumn(name = "order_id"))
private List<MenuItem> menuItems; // 메뉴 리스트 (내장 객체로 구성)

private int totalPrice; // 총 가격
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

통화 처리를 위해 BigDecimal 사용을 고려해주세요.

int 타입은 통화 계산에서 정밀도 문제를 일으킬 수 있습니다. 금융 관련 계산에는 BigDecimal을 사용하는 것이 권장됩니다.

+import java.math.BigDecimal;
+
-    private int totalPrice; // 총 가격
+    private BigDecimal totalPrice; // 총 가격
📝 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
private int totalPrice; // 총 가격
// at the top of the file, alongside the other imports
import java.math.BigDecimal;
// inside the Order entity
- private int totalPrice; // 총 가격
+ private BigDecimal totalPrice; // 총 가격
🤖 Prompt for AI Agents
In recruitment-service/src/main/java/com/example/recruitment/entity/Order.java
at line 28, the totalPrice field is currently an int, which can cause precision
issues in currency calculations. Change the type of totalPrice from int to
BigDecimal to ensure accurate financial computations. Also, update any related
code that sets or gets this field to handle BigDecimal properly.

Comment on lines 15 to 16
@Id @GeneratedValue
private Long id;
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

ID 생성 전략을 명시적으로 지정해주세요.

@GeneratedValue에 전략을 명시하지 않으면 JPA 구현체에 따라 다른 전략이 사용될 수 있습니다. 명시적으로 전략을 지정하는 것이 좋습니다.

-    @Id @GeneratedValue
+    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
     private Long id;
📝 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
@Id @GeneratedValue
private Long id;
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
🤖 Prompt for AI Agents
In recruitment-service/src/main/java/com/example/recruitment/entity/Order.java
around lines 15 to 16, the @GeneratedValue annotation on the id field lacks an
explicit generation strategy. Specify the generation strategy explicitly by
adding the strategy attribute to @GeneratedValue, such as
GenerationType.IDENTITY or GenerationType.AUTO, to ensure consistent ID
generation behavior across different JPA implementations.

Comment on lines 8 to 13
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class Order {
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

검증 어노테이션과 감사 필드 추가를 고려해주세요.

엔티티에 데이터 검증과 생성/수정 시간 추적을 위한 필드가 없습니다.

+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Positive;
+import org.springframework.data.annotation.CreatedDate;
+import org.springframework.data.annotation.LastModifiedDate;
+import org.springframework.data.jpa.domain.support.AuditingEntityListener;
+import java.time.LocalDateTime;
+
 @Entity
 @Table(name = "orders")
+@EntityListeners(AuditingEntityListener.class)
 @Getter @Setter
 @NoArgsConstructor
 @AllArgsConstructor
 public class Order {
+
+    // 기존 필드들...
+    
+    @CreatedDate
+    private LocalDateTime createdAt;
+    
+    @LastModifiedDate  
+    private LocalDateTime updatedAt;
📝 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
@Entity
@Table(name = "orders")
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class Order {
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Positive;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
@Entity
@Table(name = "orders")
@EntityListeners(AuditingEntityListener.class)
@Getter @Setter
@NoArgsConstructor
@AllArgsConstructor
public class Order {
// 기존 필드들...
@CreatedDate
private LocalDateTime createdAt;
@LastModifiedDate
private LocalDateTime updatedAt;
}
🤖 Prompt for AI Agents
In recruitment-service/src/main/java/com/example/recruitment/entity/Order.java
around lines 8 to 13, the entity lacks validation annotations and audit fields
for tracking creation and modification timestamps. Add appropriate validation
annotations (e.g., @NotNull, @Size) to relevant fields to enforce data
integrity. Also, include audit fields such as createdAt and updatedAt with
annotations like @CreatedDate and @LastModifiedDate, and enable auditing by
adding @EntityListeners(AuditingEntityListener.class) to the entity class.

Copy link

@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: 10

♻️ Duplicate comments (1)
recruitment-service/src/main/java/com/example/recruitment/entity/Recruitment.java (1)

31-31: 🛠️ Refactor suggestion

상태 필드에 열거형(Enum) 사용을 권장합니다.

이전 리뷰에서 언급된 것처럼, 문자열 대신 열거형을 사용하여 타입 안전성을 향상시키고 유효하지 않은 상태 값 입력을 방지할 수 있습니다.

🧹 Nitpick comments (5)
recruitment-service/src/main/java/com/example/recruitment/dto/RecruitmentRequestDto.java (1)

36-36: 메뉴 정보 필드에 대한 검증 고려사항

주문 생성 시 메뉴 정보가 필수인 경우, 해당 필드에도 검증 어노테이션을 추가하는 것을 고려해보세요.

+@NotEmpty(message = "메뉴 정보는 필수입니다.")
private List<OrderRequestDto.MenuDto> menus;
recruitment-service/src/main/java/com/example/recruitment/entity/Recruitment.java (1)

35-37: 카테고리 필드에 검증 제약 조건 추가를 고려하세요.

데이터 무결성을 위해 길이 제한이나 허용되는 값에 대한 제약 조건을 추가하는 것을 권장합니다.

-@Column(nullable = false)
+@Column(nullable = false, length = 50)
+@Enumerated(EnumType.STRING)  // 또는 허용된 카테고리 값들을 enum으로 정의
private String category;
recruitment-service/src/main/java/com/example/recruitment/service/RecruitmentService.java (1)

80-91: 주문 생성 실패 시 에러 처리와 엔티티 생성 방식 개선이 필요합니다.

-        // 주문 서버에 주문 생성 요청
-        orderDto.setGroupId(recruitmentId);
-        orderDto.setUserId(userId);
-        Long orderId = orderClient.createOrder(orderDto);
-
-        // 참여자 등록 + 주문 ID 저장
-        RecruitmentParticipant participant = new RecruitmentParticipant();
-        participant.setRecruitment(recruitment);
-        participant.setUser(user);
-        participant.setOrderId(orderId);
-        participantRepository.save(participant);
+        // 주문 서버에 주문 생성 요청
+        orderDto.setGroupId(recruitmentId);
+        orderDto.setUserId(userId);
+        
+        try {
+            Long orderId = orderClient.createOrder(orderDto);
+            
+            // 참여자 등록 + 주문 ID 저장
+            RecruitmentParticipant participant = RecruitmentParticipant.builder()
+                    .recruitment(recruitment)
+                    .user(user)
+                    .orderId(orderId)
+                    .build();
+            participantRepository.save(participant);
+        } catch (Exception e) {
+            log.error("주문 생성 실패. recruitmentId: {}, userId: {}", recruitmentId, userId, e);
+            throw new OrderCreationException("주문 생성에 실패했습니다.", e);
+        }
recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.java (2)

51-55: 엔드포인트 설계를 더 명확하게 개선하세요.

현재 방식은 Swagger 문서화 시 혼란을 줄 수 있습니다.

-    // 상태별 조회
-    @GetMapping(params = "status")
-    public List<Recruitment> getByStatus(@RequestParam String status) {
+    // 상태별 조회
+    @GetMapping("/by-status")
+    public List<Recruitment> getByStatus(@RequestParam String status) {

1-168: 전역 예외 처리 및 일관된 응답 형식 구현을 권장합니다.

현재 각 메서드에서 개별적으로 예외를 처리하고 있어 일관성이 부족합니다.

다음과 같은 전역 예외 처리기 구현을 고려하세요:

@RestControllerAdvice
public class GlobalExceptionHandler {
    
    @ExceptionHandler(RecruitmentNotFoundException.class)
    public ResponseEntity<ErrorResponse> handleRecruitmentNotFound(RecruitmentNotFoundException e) {
        return ResponseEntity.status(HttpStatus.NOT_FOUND)
                .body(new ErrorResponse("RECRUITMENT_NOT_FOUND", e.getMessage()));
    }
    
    @ExceptionHandler(ValidationException.class)
    public ResponseEntity<ErrorResponse> handleValidation(ValidationException e) {
        return ResponseEntity.status(HttpStatus.BAD_REQUEST)
                .body(new ErrorResponse("VALIDATION_ERROR", e.getMessage()));
    }
}

또한 일관된 응답 형식을 위한 wrapper 클래스도 고려하세요.

📜 Review details

Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d20dc23 and 5235264.

📒 Files selected for processing (11)
  • recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.java (4 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/dto/RecruitmentRequestDto.java (2 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/dto/order/OrderRequestDto.java (1 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/dto/order/OrderResponseDto.java (1 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/entity/Recruitment.java (2 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/entity/RecruitmentParticipant.java (1 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/exception/GlobalExceptionHandler.java (3 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/repository/RecruitmentParticipantRepository.java (1 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/service/OrderClient.java (1 hunks)
  • recruitment-service/src/main/java/com/example/recruitment/service/RecruitmentService.java (1 hunks)
  • recruitment-service/src/main/resources/application.properties (1 hunks)
✅ Files skipped from review due to trivial changes (4)
  • recruitment-service/src/main/resources/application.properties
  • recruitment-service/src/main/java/com/example/recruitment/dto/order/OrderResponseDto.java
  • recruitment-service/src/main/java/com/example/recruitment/entity/RecruitmentParticipant.java
  • recruitment-service/src/main/java/com/example/recruitment/dto/order/OrderRequestDto.java
🚧 Files skipped from review as they are similar to previous changes (1)
  • recruitment-service/src/main/java/com/example/recruitment/exception/GlobalExceptionHandler.java
🧰 Additional context used
🧬 Code Graph Analysis (3)
recruitment-service/src/main/java/com/example/recruitment/service/OrderClient.java (1)
recruitment-service/src/main/java/com/example/recruitment/service/RecruitmentService.java (1)
  • Service (17-92)
recruitment-service/src/main/java/com/example/recruitment/dto/RecruitmentRequestDto.java (1)
recruitment-service/src/main/java/com/example/recruitment/dto/order/OrderRequestDto.java (3)
  • Getter (8-32)
  • Getter (17-24)
  • Getter (26-31)
recruitment-service/src/main/java/com/example/recruitment/service/RecruitmentService.java (1)
recruitment-service/src/main/java/com/example/recruitment/service/OrderClient.java (1)
  • Service (11-42)
🔇 Additional comments (5)
recruitment-service/src/main/java/com/example/recruitment/repository/RecruitmentParticipantRepository.java (1)

24-25: 엔티티 참조 기반 중복 확인 메서드가 잘 구현되었습니다.

Spring Data JPA의 명명 규칙을 올바르게 따르고 있으며, RecruitmentService에서 중복 참여 검증을 위해 적절히 활용되고 있습니다.

recruitment-service/src/main/java/com/example/recruitment/dto/RecruitmentRequestDto.java (2)

32-33: 카테고리 필드 추가가 적절히 구현되었습니다.

@NotBlank 검증 어노테이션을 사용하여 필수 입력값 검증이 올바르게 설정되었습니다.


35-46: 주문 통합을 위한 변환 메서드가 잘 설계되었습니다.

OrderRequestDto로의 변환 로직이 명확하고 간단하게 구현되어 있으며, RecruitmentService에서 효과적으로 활용되고 있습니다.

recruitment-service/src/main/java/com/example/recruitment/service/RecruitmentService.java (1)

17-26: 서비스 클래스 구조가 적절합니다.

의존성 주입과 애노테이션 사용이 올바르게 구성되어 있습니다.

recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.java (1)

22-28: API 버저닝과 서비스 레이어 도입이 잘 적용되었습니다.

관심사의 분리가 개선되었고, API 버전 관리가 추가되어 향후 변경에 유연하게 대응할 수 있습니다.

@Service
public class OrderClient {

private final RestTemplate restTemplate = new RestTemplate();
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

RestTemplate을 빈으로 설정하는 것을 권장합니다.

RestTemplate을 직접 인스턴스화하는 대신 Spring 설정 클래스에서 빈으로 정의하여 의존성 주입을 받도록 개선할 수 있습니다.

Configuration 클래스에 다음과 같이 추가하세요:

@Configuration
public class RestTemplateConfig {
    
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplateBuilder()
                .setConnectTimeout(Duration.ofSeconds(5))
                .setReadTimeout(Duration.ofSeconds(10))
                .build();
    }
}

그리고 OrderClient에서는 의존성 주입을 받도록 수정하세요:

-private final RestTemplate restTemplate = new RestTemplate();
+private final RestTemplate restTemplate;

+public OrderClient(RestTemplate restTemplate) {
+    this.restTemplate = restTemplate;
+}
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/service/OrderClient.java
at line 14, the RestTemplate is instantiated directly, which is not recommended.
To fix this, define a RestTemplate bean in a separate configuration class using
RestTemplateBuilder with appropriate timeouts, then modify OrderClient to
receive the RestTemplate via dependency injection instead of creating it
manually.

Comment on lines +25 to +41
ResponseEntity<Map> response = restTemplate.postForEntity(fullUrl, requestDto, Map.class);

if (response.getStatusCode().is2xxSuccessful()) {
Map<String, Object> responseBody = response.getBody();

if (responseBody != null && responseBody.containsKey("data")) {
Map<String, Object> data = (Map<String, Object>) responseBody.get("data");

if (data != null && data.containsKey("orderId")) {
return Long.valueOf(data.get("orderId").toString());
}
}
throw new RuntimeException("응답에 orderId 없음");
}

throw new RuntimeException("주문 생성 실패: " + response.getStatusCode());
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

타입 안전성 개선 및 더 구체적인 예외 처리가 필요합니다.

현재 Map 타입을 사용한 응답 처리는 타입 안전성이 부족하고, 런타임 에러 발생 가능성이 있습니다.

OrderResponseDto를 활용하여 타입 안전성을 개선하세요:

-ResponseEntity<Map> response = restTemplate.postForEntity(fullUrl, requestDto, Map.class);
+try {
+    ResponseEntity<OrderResponseDto> response = restTemplate.postForEntity(fullUrl, requestDto, OrderResponseDto.class);
+    
+    if (response.getStatusCode().is2xxSuccessful() && response.getBody() != null) {
+        return response.getBody().getOrderId();
+    }
+    
+    throw new OrderCreationException("주문 응답이 유효하지 않습니다.");
+    
+} catch (RestClientException e) {
+    throw new OrderCreationException("주문 서버 통신 실패: " + e.getMessage(), e);
+}

OrderCreationException과 같은 커스텀 예외를 정의하여 더 구체적인 에러 처리를 구현하는 것을 권장합니다.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/service/OrderClient.java
around lines 25 to 41, the current use of raw Map for response handling lacks
type safety and risks runtime errors. Refactor the code to use a strongly typed
OrderResponseDto class that matches the expected response structure to improve
type safety. Additionally, define and throw a custom exception such as
OrderCreationException instead of generic RuntimeException to provide more
specific error handling and clearer error semantics.

Comment on lines +39 to +47
@Column(name = "order_id")
private Long orderId;

@ElementCollection
private List<Long> orderIds = new ArrayList<>();

public void addOrderId(Long orderId) {
this.orderIds.add(orderId);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

주문 ID 필드 설계를 재검토해야 합니다.

orderIdorderIds 필드가 동시에 존재하는 것이 혼란을 야기할 수 있습니다. 모집글당 하나의 주문 ID만 필요한지, 아니면 여러 주문을 지원해야 하는지 명확히 하여 설계를 단순화하는 것을 권장합니다.

RecruitmentService의 구현을 보면 orderId 필드만 사용되고 있으므로, 다음 중 하나를 선택하세요:

옵션 1: 단일 주문 ID만 지원하는 경우

-@Column(name = "order_id")
-private Long orderId;
-
-@ElementCollection
-private List<Long> orderIds = new ArrayList<>();
-
-public void addOrderId(Long orderId) {
-    this.orderIds.add(orderId);
-}
+@Column(name = "order_id")
+private Long orderId;

옵션 2: 다중 주문 ID를 지원하는 경우

-@Column(name = "order_id")
-private Long orderId;
-
-@ElementCollection
-private List<Long> orderIds = new ArrayList<>();
+@ElementCollection
+@Column(name = "order_id")
+private List<Long> orderIds = new ArrayList<>();
📝 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
@Column(name = "order_id")
private Long orderId;
@ElementCollection
private List<Long> orderIds = new ArrayList<>();
public void addOrderId(Long orderId) {
this.orderIds.add(orderId);
}
@Column(name = "order_id")
private Long orderId;
Suggested change
@Column(name = "order_id")
private Long orderId;
@ElementCollection
private List<Long> orderIds = new ArrayList<>();
public void addOrderId(Long orderId) {
this.orderIds.add(orderId);
}
@ElementCollection
@Column(name = "order_id")
private List<Long> orderIds = new ArrayList<>();
public void addOrderId(Long orderId) {
this.orderIds.add(orderId);
}
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/entity/Recruitment.java
around lines 39 to 47, there is confusion due to having both orderId and
orderIds fields. Decide whether to support a single order ID or multiple order
IDs per recruitment entity. If only one order ID is needed, remove the orderIds
list and related methods. If multiple order IDs are required, remove the single
orderId field and ensure all code uses the orderIds list consistently.

Comment on lines +36 to +48
User user = userRepository.findById(dto.getUserId())
.orElseThrow(() -> new RuntimeException("사용자 없음"));
Store store = storeRepository.findById(dto.getStoreId())
.orElseThrow(() -> new RuntimeException("가게 없음"));

Recruitment recruitment = new Recruitment();
recruitment.setUser(user);
recruitment.setStore(store);
recruitment.setTitle(dto.getTitle());
recruitment.setDescription(dto.getDescription());
recruitment.setDeadlineTime(dto.getDeadlineTime());
recruitment.setStatus("RECRUITING");
recruitment.setCategory(dto.getCategory());
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 처리 및 상태값 관리 개선이 필요합니다.

  1. RuntimeException 대신 도메인 특화 예외 클래스 사용을 권장합니다.
  2. "RECRUITING" 같은 상태값은 상수나 enum으로 관리하세요.
  3. setter 대신 빌더 패턴이나 정적 팩토리 메서드 사용을 고려하세요.
-        User user = userRepository.findById(dto.getUserId())
-                .orElseThrow(() -> new RuntimeException("사용자 없음"));
-        Store store = storeRepository.findById(dto.getStoreId())
-                .orElseThrow(() -> new RuntimeException("가게 없음"));
+        User user = userRepository.findById(dto.getUserId())
+                .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다. ID: " + dto.getUserId()));
+        Store store = storeRepository.findById(dto.getStoreId())
+                .orElseThrow(() -> new StoreNotFoundException("가게를 찾을 수 없습니다. ID: " + dto.getStoreId()));

상태값을 enum으로 관리하는 예시:

public enum RecruitmentStatus {
    RECRUITING("RECRUITING"),
    CONFIRMED("CONFIRMED"),
    FAILED("FAILED"),
    ACCEPTED("ACCEPTED"),
    DELIVERED("DELIVERED");
    
    private final String value;
    // constructor, getter...
}
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/service/RecruitmentService.java
around lines 36 to 48, replace the generic RuntimeException with domain-specific
exception classes for user and store not found cases. Manage the recruitment
status using an enum instead of a hardcoded string "RECRUITING". Refactor the
Recruitment object creation to use a builder pattern or a static factory method
instead of multiple setter calls for better immutability and clarity.

Comment on lines +70 to +78
Recruitment recruitment = recruitmentRepository.findById(recruitmentId)
.orElseThrow(() -> new RuntimeException("모집 없음"));
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("사용자 없음"));

boolean alreadyJoined = participantRepository.existsByRecruitmentAndUser(recruitment, user);
if (alreadyJoined) {
throw new RuntimeException("이미 참여한 모집입니다.");
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

예외 처리 개선이 필요합니다.

도메인 특화 예외를 사용하여 더 명확한 에러 처리를 구현하세요.

-        Recruitment recruitment = recruitmentRepository.findById(recruitmentId)
-                .orElseThrow(() -> new RuntimeException("모집 없음"));
-        User user = userRepository.findById(userId)
-                .orElseThrow(() -> new RuntimeException("사용자 없음"));
+        Recruitment recruitment = recruitmentRepository.findById(recruitmentId)
+                .orElseThrow(() -> new RecruitmentNotFoundException("모집글을 찾을 수 없습니다. ID: " + recruitmentId));
+        User user = userRepository.findById(userId)
+                .orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다. ID: " + userId));

-            throw new RuntimeException("이미 참여한 모집입니다.");
+            throw new DuplicateParticipationException("이미 참여한 모집입니다. recruitmentId: " + recruitmentId + ", userId: " + userId);
📝 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
Recruitment recruitment = recruitmentRepository.findById(recruitmentId)
.orElseThrow(() -> new RuntimeException("모집 없음"));
User user = userRepository.findById(userId)
.orElseThrow(() -> new RuntimeException("사용자 없음"));
boolean alreadyJoined = participantRepository.existsByRecruitmentAndUser(recruitment, user);
if (alreadyJoined) {
throw new RuntimeException("이미 참여한 모집입니다.");
}
Recruitment recruitment = recruitmentRepository.findById(recruitmentId)
.orElseThrow(() -> new RecruitmentNotFoundException("모집글을 찾을 수 없습니다. ID: " + recruitmentId));
User user = userRepository.findById(userId)
.orElseThrow(() -> new UserNotFoundException("사용자를 찾을 수 없습니다. ID: " + userId));
boolean alreadyJoined = participantRepository.existsByRecruitmentAndUser(recruitment, user);
if (alreadyJoined) {
throw new DuplicateParticipationException("이미 참여한 모집입니다. recruitmentId: "
+ recruitmentId + ", userId: " + userId);
}
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/service/RecruitmentService.java
around lines 70 to 78, replace the generic RuntimeException with domain-specific
custom exceptions for cases like "모집 없음", "사용자 없음", and "이미 참여한 모집입니다." Define
and throw meaningful exceptions such as RecruitmentNotFoundException,
UserNotFoundException, and ParticipantAlreadyExistsException to improve clarity
and error handling.

Comment on lines +49 to +59
recruitmentRepository.save(recruitment);

// 주문 서버에 주문 생성 요청
OrderRequestDto orderDto = dto.toOrderRequestDto();
orderDto.setGroupId(recruitment.getId());
Long orderId = orderClient.createOrder(orderDto);

// orderId를 모집글에 저장
recruitment.setOrderId(orderId);
recruitmentRepository.save(recruitment);
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

⚠️ Potential issue

중복 저장 및 트랜잭션 처리 개선이 필요합니다.

현재 구현은 recruitment를 두 번 저장하고 있으며, 외부 API 호출 실패 시 불완전한 데이터가 남을 수 있습니다.

-        recruitment.setStatus("RECRUITING");
-        recruitment.setCategory(dto.getCategory());
-        recruitmentRepository.save(recruitment);
-
-        // 주문 서버에 주문 생성 요청
-        OrderRequestDto orderDto = dto.toOrderRequestDto();
-        orderDto.setGroupId(recruitment.getId());
-        Long orderId = orderClient.createOrder(orderDto);
-
-        // orderId를 모집글에 저장
-        recruitment.setOrderId(orderId);
-        recruitmentRepository.save(recruitment);
+        recruitment.setStatus(RecruitmentStatus.RECRUITING.getValue());
+        recruitment.setCategory(dto.getCategory());
+        
+        // 주문 서버에 주문 생성 요청
+        OrderRequestDto orderDto = dto.toOrderRequestDto();
+        
+        try {
+            // 임시 저장하여 ID 생성
+            recruitment = recruitmentRepository.save(recruitment);
+            orderDto.setGroupId(recruitment.getId());
+            
+            Long orderId = orderClient.createOrder(orderDto);
+            recruitment.setOrderId(orderId);
+            recruitmentRepository.save(recruitment);
+        } catch (Exception e) {
+            // 주문 생성 실패 시 로깅
+            log.error("주문 생성 실패. recruitmentId: {}", recruitment.getId(), e);
+            throw new OrderCreationException("주문 생성에 실패했습니다.", e);
+        }
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/service/RecruitmentService.java
around lines 49 to 59, the recruitment entity is saved twice, which is redundant
and risks data inconsistency if the external orderClient.createOrder call fails.
Refactor to save the recruitment entity only once after successfully obtaining
the orderId from the external API. Additionally, ensure the method is wrapped in
a transaction to maintain atomicity, so that either all operations succeed or
none are persisted.

Comment on lines +36 to 43
//모집글 참여 (Order 서버에 주문 생성 포함)
@PostMapping("/{recruitmentId}/join")
public ResponseEntity<?> joinRecruitment(@PathVariable Long recruitmentId,
@RequestParam Long userId,
@RequestBody OrderRequestDto orderRequestDto) {
recruitmentService.joinRecruitment(recruitmentId, userId, orderRequestDto);
return ResponseEntity.ok("모집글 참여 완료");
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

API 설계 개선이 필요합니다.

userId를 쿼리 파라미터로 받는 것보다 인증 정보에서 추출하는 것이 보안상 더 안전합니다.

-    @PostMapping("/{recruitmentId}/join")
-    public ResponseEntity<?> joinRecruitment(@PathVariable Long recruitmentId,
-                                             @RequestParam Long userId,
-                                             @RequestBody OrderRequestDto orderRequestDto) {
-        recruitmentService.joinRecruitment(recruitmentId, userId, orderRequestDto);
+    @PostMapping("/{recruitmentId}/join")
+    public ResponseEntity<?> joinRecruitment(@PathVariable Long recruitmentId,
+                                             @RequestBody OrderRequestDto orderRequestDto,
+                                             @AuthenticationPrincipal UserDetails userDetails) {
+        Long userId = getUserIdFromAuth(userDetails);
+        recruitmentService.joinRecruitment(recruitmentId, userId, orderRequestDto);

향후 Spring Security를 통한 인증 구현을 고려하세요.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.java
around lines 36 to 43, the userId is currently received as a request parameter,
which is less secure. Modify the method to extract the userId from the
authentication principal or security context instead of accepting it as a query
parameter. This involves integrating Spring Security or a similar authentication
mechanism to obtain the authenticated user's ID securely within the controller
method.

Comment on lines +161 to +163
if (dto.getStoreId() != null) {
recruitment.setStore(recruitment.getStore());
}
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

치명적인 버그: Store 업데이트가 작동하지 않습니다.

현재 코드는 자기 자신을 설정하고 있어 아무런 변경이 일어나지 않습니다.

         if (dto.getStoreId() != null) {
-            recruitment.setStore(recruitment.getStore());
+            Store newStore = storeRepository.findById(dto.getStoreId())
+                    .orElseThrow(() -> new StoreNotFoundException("가게를 찾을 수 없습니다. ID: " + dto.getStoreId()));
+            recruitment.setStore(newStore);
         }

이를 위해 컨트롤러에 StoreRepository 의존성을 다시 추가하거나, 서비스 레이어에서 처리하도록 리팩토링하세요.

📝 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
if (dto.getStoreId() != null) {
recruitment.setStore(recruitment.getStore());
}
if (dto.getStoreId() != null) {
Store newStore = storeRepository.findById(dto.getStoreId())
.orElseThrow(() -> new StoreNotFoundException("가게를 찾을 수 없습니다. ID: " + dto.getStoreId()));
recruitment.setStore(newStore);
}
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.java
around lines 161 to 163, the code incorrectly sets
recruitment.setStore(recruitment.getStore()), which does not update the store.
To fix this, inject StoreRepository into the controller or delegate the store
update logic to the service layer, then fetch the Store entity by
dto.getStoreId() and set it on recruitment properly.

Comment on lines +57 to +79
// 모집글 상세 조회
@GetMapping("/{recruitmentId}")
public ResponseEntity<?> getRecruitmentDetail(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();

// 참여자 목록 조회
List<RecruitmentParticipant> participantEntities =
participantRepository.findByRecruitmentId(recruitmentId);

List<User> participants = participantEntities.stream()
.map(RecruitmentParticipant::getUser)
.toList();

// DTO 구성
RecruitmentDetailDto dto = new RecruitmentDetailDto();
dto.setId(recruitment.getId());
dto.setTitle(recruitment.getTitle());
dto.setDescription(recruitment.getDescription());
dto.setStatus(recruitment.getStatus());
dto.setDeadlineTime(recruitment.getDeadlineTime());
dto.setUser(recruitment.getUser());
dto.setStore(recruitment.getStore());
dto.setParticipants(participants);

return ResponseEntity.ok(dto);
}

@GetMapping("/user/{userId}/created-recruitments")
public List<Recruitment> getRecruitmentsCreatedByUser(@PathVariable Long userId) {
return recruitmentRepository.findByUserId(userId);
}

// 특정 유저가 참여한 모집글 조회
@GetMapping("/user/{userId}/joined-recruitments")
public List<Recruitment> getRecruitmentsJoinedByUser(@PathVariable Long userId) {
List<RecruitmentParticipant> participantList = participantRepository.findByUserId(userId);
return participantList.stream()
.map(RecruitmentParticipant::getRecruitment)
.toList();
}




// 모집글 참여
@PostMapping("/{recruitmentId}/join")
public ResponseEntity<?> joinRecruitment(@PathVariable Long recruitmentId, @RequestParam Long userId) {
User user = userRepository.findById(userId).orElseThrow();
public ResponseEntity<?> getRecruitmentDetail(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();

boolean alreadyJoined = participantRepository
.findByUserIdAndRecruitmentId(userId, recruitmentId)
.isPresent();

if (alreadyJoined) {
return ResponseEntity.badRequest().body("이미 참여한 모집입니다.");
}
List<RecruitmentParticipant> participants =
participantRepository.findByRecruitmentId(recruitmentId);
List<User> participantUsers = participants.stream()
.map(RecruitmentParticipant::getUser)
.toList();

RecruitmentDetailDto dto = new RecruitmentDetailDto();
dto.setId(recruitment.getId());
dto.setTitle(recruitment.getTitle());
dto.setDescription(recruitment.getDescription());
dto.setStatus(recruitment.getStatus());
dto.setDeadlineTime(recruitment.getDeadlineTime());
dto.setUser(recruitment.getUser());
dto.setStore(recruitment.getStore());
dto.setParticipants(participantUsers);

return ResponseEntity.ok(dto);
}
Copy link

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

DTO 생성 방식 개선 및 성능 최적화가 필요합니다.

-        RecruitmentDetailDto dto = new RecruitmentDetailDto();
-        dto.setId(recruitment.getId());
-        dto.setTitle(recruitment.getTitle());
-        dto.setDescription(recruitment.getDescription());
-        dto.setStatus(recruitment.getStatus());
-        dto.setDeadlineTime(recruitment.getDeadlineTime());
-        dto.setUser(recruitment.getUser());
-        dto.setStore(recruitment.getStore());
-        dto.setParticipants(participantUsers);
+        RecruitmentDetailDto dto = RecruitmentDetailDto.builder()
+                .id(recruitment.getId())
+                .title(recruitment.getTitle())
+                .description(recruitment.getDescription())
+                .status(recruitment.getStatus())
+                .deadlineTime(recruitment.getDeadlineTime())
+                .user(recruitment.getUser())
+                .store(recruitment.getStore())
+                .participants(participantUsers)
+                .build();

N+1 쿼리 문제 확인:


🏁 Script executed:

#!/bin/bash
# RecruitmentParticipantRepository에서 fetch join 사용 여부 확인
rg -A 5 "findByRecruitmentId" --glob "*.java"

Length of output: 1776


🏁 Script executed:

#!/bin/bash
# RecruitmentDetailDto 클래스 정의 및 Lombok @Builder 어노테이션 확인
rg -A 5 "class RecruitmentDetailDto" --glob "*.java"
rg "@Builder" --glob "*RecruitmentDetailDto.java"

Length of output: 814


Builder 패턴 적용 및 N+1 쿼리 최적화 필요

RecruitmentDetailDto에 Lombok @builder가 적용되어 있지 않아 제안한 builder 생성 코드가 동작하지 않으며, findByRecruitmentId 호출 시 RecruitmentParticipant.user가 LAZY 로딩일 경우 participants 수만큼 추가 쿼리가 발생할 수 있습니다.

• RecruitmentDetailDto 클래스(recruitment-service/src/main/java/com/example/recruitment/dto/RecruitmentDetailDto.java)
– Lombok @builder, @AllArgsConstructor, @NoArgsConstructor 어노테이션 추가 또는 직접 Builder 구현
예시:

@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RecruitmentDetailDto { … }

• RecruitmentParticipantRepository 인터페이스(recruitment-service/src/main/java/com/example/recruitment/repository/RecruitmentParticipantRepository.java)
– fetch join을 사용해 N+1 문제 방지

@Query("SELECT p FROM RecruitmentParticipant p JOIN FETCH p.user WHERE p.recruitment.id = :recruitmentId")
List<RecruitmentParticipant> findByRecruitmentIdFetchUser(@Param("recruitmentId") Long recruitmentId);

또는 Spring Data JPA EntityGraph 사용

위 사항을 반영해 DTO 생성과 데이터 조회 방식을 개선해주세요.

🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.java
lines 57-79, refactor the DTO creation to use the Lombok @Builder pattern by
first adding @Builder, @NoArgsConstructor, and @AllArgsConstructor annotations
to the RecruitmentDetailDto class. Also, modify the participantRepository query
to use a fetch join to eagerly load the User entities and avoid N+1 queries by
defining a new method like findByRecruitmentIdFetchUser with a JPQL query using
JOIN FETCH. Then update the controller to call this new repository method and
build the RecruitmentDetailDto using the builder for cleaner and more efficient
code.

Comment on lines +29 to +34
//모집글 생성 (Order 서버에 주문 생성 포함)
@PostMapping
public Recruitment createRecruitment(@Valid @RequestBody RecruitmentRequestDto dto) {
User user = userRepository.findById(dto.getUserId()).orElseThrow();
Store store = storeRepository.findById(dto.getStoreId()).orElseThrow();

Recruitment recruitment = new Recruitment();
recruitment.setUser(user);
recruitment.setStore(store);
recruitment.setTitle(dto.getTitle());
recruitment.setDescription(dto.getDescription());
recruitment.setDeadlineTime(dto.getDeadlineTime());
recruitment.setStatus("RECRUITING");
public ResponseEntity<?> createRecruitment(@Valid @RequestBody RecruitmentRequestDto dto) {
recruitmentService.createRecruitment(dto);
return ResponseEntity.ok("모집글 생성 완료");
}
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

RESTful API 모범 사례를 따르도록 개선이 필요합니다.

생성된 리소스의 정보를 반환하고 적절한 HTTP 상태 코드를 사용하세요.

-    public ResponseEntity<?> createRecruitment(@Valid @RequestBody RecruitmentRequestDto dto) {
-        recruitmentService.createRecruitment(dto);
-        return ResponseEntity.ok("모집글 생성 완료");
+    public ResponseEntity<?> createRecruitment(@Valid @RequestBody RecruitmentRequestDto dto) {
+        Long recruitmentId = recruitmentService.createRecruitment(dto);
+        return ResponseEntity.created(URI.create("/api/v1/recruitments/" + recruitmentId))
+                .body(Map.of("id", recruitmentId, "message", "모집글이 생성되었습니다"));

서비스 메서드도 생성된 ID를 반환하도록 수정이 필요합니다.

📝 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
//모집글 생성 (Order 서버에 주문 생성 포함)
@PostMapping
public Recruitment createRecruitment(@Valid @RequestBody RecruitmentRequestDto dto) {
User user = userRepository.findById(dto.getUserId()).orElseThrow();
Store store = storeRepository.findById(dto.getStoreId()).orElseThrow();
Recruitment recruitment = new Recruitment();
recruitment.setUser(user);
recruitment.setStore(store);
recruitment.setTitle(dto.getTitle());
recruitment.setDescription(dto.getDescription());
recruitment.setDeadlineTime(dto.getDeadlineTime());
recruitment.setStatus("RECRUITING");
public ResponseEntity<?> createRecruitment(@Valid @RequestBody RecruitmentRequestDto dto) {
recruitmentService.createRecruitment(dto);
return ResponseEntity.ok("모집글 생성 완료");
}
//모집글 생성 (Order 서버에 주문 생성 포함)
@PostMapping
public ResponseEntity<?> createRecruitment(@Valid @RequestBody RecruitmentRequestDto dto) {
Long recruitmentId = recruitmentService.createRecruitment(dto);
return ResponseEntity.created(URI.create("/api/v1/recruitments/" + recruitmentId))
.body(Map.of("id", recruitmentId, "message", "모집글이 생성되었습니다"));
}
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.java
around lines 29 to 34, the createRecruitment method currently returns a generic
success message with HTTP 200. Modify the recruitmentService.createRecruitment
method to return the ID of the newly created resource, then update the
controller to return this ID in the response body with HTTP status 201 Created,
following RESTful best practices.

@ehddms0320 ehddms0320 changed the title feat: Swagger UI 설정 및 recruitment API 문서화 feat: 모집 생성 및 참여 시 주문 생성 기능 구현 May 30, 2025
@ehddms0320 ehddms0320 changed the title feat: 모집 생성 및 참여 시 주문 생성 기능 구현 feat: Swagger 설정 및 모집글 API 문서화 완료 May 30, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants