Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 3 additions & 2 deletions recruitment-service/build.gradle
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.5'
id 'io.spring.dependency-management' version '1.1.7'
id 'org.springframework.boot' version '3.1.5'
id 'io.spring.dependency-management' version '1.1.3'
}

group = 'com.example'
Expand All @@ -28,6 +28,7 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.2.0'
compileOnly 'org.projectlombok:lombok'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
runtimeOnly 'com.mysql:mysql-connector-j'
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,13 @@

import com.example.recruitment.dto.RecruitmentDetailDto;
import com.example.recruitment.dto.RecruitmentRequestDto;
import com.example.recruitment.dto.order.OrderRequestDto;
import com.example.recruitment.entity.Recruitment;
import com.example.recruitment.entity.RecruitmentParticipant;
import com.example.recruitment.entity.Store;
import com.example.recruitment.entity.User;
import com.example.recruitment.repository.RecruitmentParticipantRepository;
import com.example.recruitment.repository.RecruitmentRepository;
import com.example.recruitment.repository.StoreRepository;
import com.example.recruitment.repository.UserRepository;

import com.example.recruitment.service.RecruitmentService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
Expand All @@ -21,29 +19,27 @@

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/recruitments")
@RequestMapping("/api/v1/recruitments")
public class RecruitmentController {

private final RecruitmentService recruitmentService;
private final RecruitmentRepository recruitmentRepository;
private final UserRepository userRepository;
private final StoreRepository storeRepository;
private final RecruitmentParticipantRepository participantRepository;

// 모집글 생성
//모집글 생성 (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("모집글 생성 완료");
}

return recruitmentRepository.save(recruitment);
//모집글 참여 (Order 서버에 주문 생성 포함)
@PostMapping("/{recruitmentId}/join")
public ResponseEntity<?> joinRecruitment(@PathVariable Long recruitmentId,
@RequestParam Long userId,
@RequestBody OrderRequestDto orderRequestDto) {
recruitmentService.joinRecruitment(recruitmentId, userId, orderRequestDto);
return ResponseEntity.ok("모집글 참여 완료");
}
Comment on lines +36 to 43
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 원칙에 맞게 구현되었고, 서비스 계층으로의 위임도 적절합니다.

보안상 userId를 URL 파라미터 대신 인증된 사용자 정보에서 가져오는 것을 권장합니다:

-public ResponseEntity<?> joinRecruitment(@PathVariable Long recruitmentId,
-                                         @RequestParam Long userId,
-                                         @RequestBody OrderRequestDto orderRequestDto) {
+public ResponseEntity<?> joinRecruitment(@PathVariable Long recruitmentId,
+                                         @RequestBody OrderRequestDto orderRequestDto,
+                                         Authentication authentication) {
+    Long userId = getUserIdFromAuthentication(authentication);
📝 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("/{recruitmentId}/join")
public ResponseEntity<?> joinRecruitment(@PathVariable Long recruitmentId,
@RequestParam Long userId,
@RequestBody OrderRequestDto orderRequestDto) {
recruitmentService.joinRecruitment(recruitmentId, userId, orderRequestDto);
return ResponseEntity.ok("모집글 참여 완료");
}
//모집글 참여 (Order 서버에 주문 생성 포함)
@PostMapping("/{recruitmentId}/join")
public ResponseEntity<?> joinRecruitment(@PathVariable Long recruitmentId,
@RequestBody OrderRequestDto orderRequestDto,
Authentication authentication) {
Long userId = getUserIdFromAuthentication(authentication);
recruitmentService.joinRecruitment(recruitmentId, userId, orderRequestDto);
return ResponseEntity.ok("모집글 참여 완료");
}
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.java
around lines 36 to 43, the joinRecruitment method currently accepts userId as a
request parameter, which is a security risk. Modify the method to remove the
userId parameter and instead obtain the authenticated user's ID from the
security context or authentication principal. This ensures the userId is
securely retrieved from the logged-in user's session rather than from client
input.


// 모집글 전체 조회
Expand All @@ -52,84 +48,56 @@ public List<Recruitment> getAll() {
return recruitmentRepository.findAll();
}

// 상태별 조회 (ex. /api/recruitments?status=RECRUITING)
// 상태별 조회
@GetMapping(params = "status")
public List<Recruitment> getByStatus(@RequestParam String status) {
return recruitmentRepository.findByStatus(status);
}

// 모집글 상세 조회
@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);
}

RecruitmentParticipant participant = new RecruitmentParticipant();
participant.setUser(user);
participant.setRecruitment(recruitment);
participant.setJoinedAt(LocalDateTime.now());
// 유저가 만든 모집글
@GetMapping("/user/{userId}/created-recruitments")
public List<Recruitment> getRecruitmentsCreatedByUser(@PathVariable Long userId) {
return recruitmentRepository.findByUserId(userId);
}

participantRepository.save(participant);
return ResponseEntity.ok("모집글 참여 완료");
// 유저가 참여한 모집글
@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();
}

// 모집 상태 업데이트 (CONFIRMED 또는 FAILED)
// 모집 상태 업데이트
@PatchMapping("/{recruitmentId}/status")
public ResponseEntity<?> updateRecruitmentStatus(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();
LocalDateTime now = LocalDateTime.now();

long participantCount = participantRepository.countByRecruitmentId(recruitmentId);

if (now.isAfter(recruitment.getDeadlineTime())) {
Expand All @@ -145,68 +113,56 @@ public ResponseEntity<?> updateRecruitmentStatus(@PathVariable Long recruitmentI
}
}

// 주문 수락 상태로 변경 (ACCEPTED)
// 주문 수락 상태 변경
@PatchMapping("/{recruitmentId}/accept")
public ResponseEntity<?> acceptRecruitment(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();

if (!"CONFIRMED".equals(recruitment.getStatus())) {
return ResponseEntity.badRequest().body("주문 수락은 CONFIRMED 상태에서만 가능합니다.");
}

recruitment.setStatus("ACCEPTED");
recruitmentRepository.save(recruitment);

return ResponseEntity.ok("상태가 ACCEPTED로 변경되었습니다.");
}

// 배달 완료 상태로 변경 (DELIVERED)
// 배달 완료 상태 변경
@PatchMapping("/{recruitmentId}/deliver")
public ResponseEntity<?> completeDelivery(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();

if (!"ACCEPTED".equals(recruitment.getStatus())) {
return ResponseEntity.badRequest().body("배달 완료는 ACCEPTED 상태에서만 가능합니다.");
}

recruitment.setStatus("DELIVERED");
recruitmentRepository.save(recruitment);

return ResponseEntity.ok("상태가 DELIVERED로 변경되었습니다.");
}
// 모집글 삭제
@DeleteMapping("/{recruitmentId}")
public ResponseEntity<?> deleteRecruitment(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();

recruitmentRepository.delete(recruitment);
return ResponseEntity.ok("모집글이 삭제되었습니다.");
}

// 모집글 수정
@PutMapping("/{recruitmentId}")
public ResponseEntity<?> updateRecruitment(@PathVariable Long recruitmentId,
@Valid @RequestBody RecruitmentRequestDto dto) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();

// (선택) 본인 작성한 글인지 확인
if (!recruitment.getUser().getId().equals(dto.getUserId())) {
return ResponseEntity.status(403).body("작성자만 수정할 수 있습니다.");
// 모집글 삭제
@DeleteMapping("/{recruitmentId}")
public ResponseEntity<?> deleteRecruitment(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();
recruitmentRepository.delete(recruitment);
return ResponseEntity.ok("모집글이 삭제되었습니다.");
}

// 수정할 항목들 업데이트
recruitment.setTitle(dto.getTitle());
recruitment.setDescription(dto.getDescription());
recruitment.setDeadlineTime(dto.getDeadlineTime());
// 모집글 수정
@PutMapping("/{recruitmentId}")
public ResponseEntity<?> updateRecruitment(@PathVariable Long recruitmentId,
@Valid @RequestBody RecruitmentRequestDto dto) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();
if (!recruitment.getUser().getId().equals(dto.getUserId())) {
return ResponseEntity.status(403).body("작성자만 수정할 수 있습니다.");
}

// (선택) 가게 변경도 허용
if (dto.getStoreId() != null) {
Store store = storeRepository.findById(dto.getStoreId()).orElseThrow();
recruitment.setStore(store);
}
recruitment.setTitle(dto.getTitle());
recruitment.setDescription(dto.getDescription());
recruitment.setDeadlineTime(dto.getDeadlineTime());

recruitmentRepository.save(recruitment);
return ResponseEntity.ok("모집글이 수정되었습니다.");
}
if (dto.getStoreId() != null) {
recruitment.setStore(recruitment.getStore());
}
Comment on lines +161 to +163
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue

치명적인 로직 버그가 있습니다

새로운 storeId가 제공되었을 때 기존 store를 다시 설정하고 있어서 store 변경이 전혀 적용되지 않습니다.

다음과 같이 수정해야 합니다:

         if (dto.getStoreId() != null) {
-            recruitment.setStore(recruitment.getStore());
+            Store store = storeRepository.findById(dto.getStoreId())
+                    .orElseThrow(() -> new EntityNotFoundException("Store not found"));
+            recruitment.setStore(store);
         }

이를 위해 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 store = storeRepository.findById(dto.getStoreId())
.orElseThrow(() -> new EntityNotFoundException("Store not found"));
recruitment.setStore(store);
}
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.java
around lines 161 to 163, the code incorrectly resets the store to the existing
one when a new storeId is provided, so the store change is not applied. Fix this
by injecting StoreRepository into the controller, then fetch the new Store
entity using the provided storeId from dto and set it on the recruitment object
instead of resetting the existing store.


recruitmentRepository.save(recruitment);
return ResponseEntity.ok("모집글이 수정되었습니다.");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.recruitment.controller;

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/swagger-test")
public class SwaggerTestController {
@GetMapping
public String test() {
return "Swagger works!";
}
}
Original file line number Diff line number Diff line change
@@ -1,13 +1,14 @@
package com.example.recruitment.dto;

import com.example.recruitment.dto.order.OrderRequestDto;
import lombok.Getter;
import lombok.Setter;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;

import java.time.LocalDateTime;

import lombok.Getter;
import lombok.Setter;
import java.util.List;

@Getter
@Setter
Expand All @@ -27,25 +28,19 @@ public class RecruitmentRequestDto {

@NotNull(message = "마감 시간은 필수입니다.")
private LocalDateTime deadlineTime;
}



//원래 dto 코드
/*
* package com.example.recruitment.dto;
@NotBlank(message = "카테고리는 비어 있을 수 없습니다.")
private String category;

import java.time.LocalDateTime;
// 주문용 메뉴 정보
private List<OrderRequestDto.MenuDto> menus;
Comment on lines +35 to +36
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

메뉴 필드에 검증 추가를 권장합니다.

메뉴 정보는 주문 생성에 필수적인 데이터이므로 적절한 검증이 필요합니다.

메뉴 필드에 검증 어노테이션을 추가하는 것을 권장합니다:

 // 주문용 메뉴 정보
+@NotEmpty(message = "메뉴 정보는 필수입니다.")
+@Valid
 private List<OrderRequestDto.MenuDto> menus;
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/dto/RecruitmentRequestDto.java
at lines 35-36, the menus field lacks validation annotations. Since menu
information is essential for order creation, add appropriate validation
annotations such as @NotNull and @NotEmpty to the menus field to ensure it is
not null or empty during data binding or request processing.


import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class RecruitmentRequestDto {
private Long userId;
private Long storeId;
private String title;
private String description;
private LocalDateTime deadlineTime;
//OrderRequestDto 변환 메서드
public OrderRequestDto toOrderRequestDto() {
OrderRequestDto dto = new OrderRequestDto();
dto.setUserId(this.userId);
dto.setStoreId(this.storeId);
dto.setMenus(this.menus);
return dto;
}
}
*/
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
package com.example.recruitment.dto.order;

import java.util.List;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter

public class OrderRequestDto {
private Long groupId;
private Long userId;
private Long storeId;
private List<MenuDto> menus;

@Getter @Setter
public static class MenuDto {
private Long menuId;
private String menuName;
private int basePrice;
private int count;
private List<OptionDto> options;
}

@Getter @Setter
public static class OptionDto {
private Long optionId;
private String optionName;
private int price;
}
}
Comment on lines +11 to +32
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

데이터 검증 어노테이션을 추가하세요.

DTO에 검증 어노테이션이 없어서 잘못된 데이터가 전달될 수 있습니다. 필수 필드와 값 범위에 대한 검증을 추가하는 것이 좋습니다.

+import jakarta.validation.Valid;
+import jakarta.validation.constraints.*;

 public class OrderRequestDto {
+    @NotNull(message = "그룹 ID는 필수입니다")
     private Long groupId;
+    @NotNull(message = "사용자 ID는 필수입니다")
     private Long userId;
+    @NotNull(message = "가게 ID는 필수입니다")
     private Long storeId;
+    @NotEmpty(message = "메뉴는 최소 1개 이상이어야 합니다")
+    @Valid
     private List<MenuDto> menus;

     @Getter @Setter
     public static class MenuDto {
+        @NotNull(message = "메뉴 ID는 필수입니다")
         private Long menuId;
+        @NotBlank(message = "메뉴명은 필수입니다")
         private String menuName;
+        @Min(value = 0, message = "기본 가격은 0 이상이어야 합니다")
         private int basePrice;
+        @Min(value = 1, message = "수량은 1 이상이어야 합니다")
         private int count;
+        @Valid
         private List<OptionDto> options;
     }

     @Getter @Setter
     public static class OptionDto {
+        @NotNull(message = "옵션 ID는 필수입니다")
         private Long optionId;
+        @NotBlank(message = "옵션명은 필수입니다")
         private String optionName;
+        @Min(value = 0, message = "옵션 가격은 0 이상이어야 합니다")
         private int price;
     }
 }
📝 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
public class OrderRequestDto {
private Long groupId;
private Long userId;
private Long storeId;
private List<MenuDto> menus;
@Getter @Setter
public static class MenuDto {
private Long menuId;
private String menuName;
private int basePrice;
private int count;
private List<OptionDto> options;
}
@Getter @Setter
public static class OptionDto {
private Long optionId;
private String optionName;
private int price;
}
}
import jakarta.validation.Valid;
import jakarta.validation.constraints.*;
public class OrderRequestDto {
@NotNull(message = "그룹 ID는 필수입니다")
private Long groupId;
@NotNull(message = "사용자 ID는 필수입니다")
private Long userId;
@NotNull(message = "가게 ID는 필수입니다")
private Long storeId;
@NotEmpty(message = "메뉴는 최소 1개 이상이어야 합니다")
@Valid
private List<MenuDto> menus;
@Getter @Setter
public static class MenuDto {
@NotNull(message = "메뉴 ID는 필수입니다")
private Long menuId;
@NotBlank(message = "메뉴명은 필수입니다")
private String menuName;
@Min(value = 0, message = "기본 가격은 0 이상이어야 합니다")
private int basePrice;
@Min(value = 1, message = "수량은 1 이상이어야 합니다")
private int count;
@Valid
private List<OptionDto> options;
}
@Getter @Setter
public static class OptionDto {
@NotNull(message = "옵션 ID는 필수입니다")
private Long optionId;
@NotBlank(message = "옵션명은 필수입니다")
private String optionName;
@Min(value = 0, message = "옵션 가격은 0 이상이어야 합니다")
private int price;
}
}
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/dto/order/OrderRequestDto.java
between lines 11 and 32, the DTO classes lack validation annotations, which can
allow invalid data to be passed. Add appropriate validation annotations such as
@NotNull for mandatory fields, @Min and @Max for numeric ranges, and @NotEmpty
or @Size for collections and strings to enforce required constraints on groupId,
userId, storeId, menus, and the fields inside MenuDto and OptionDto classes.

Loading