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
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.example.recruitment.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;

@Configuration
public class RestTemplateConfig {
@Bean
public RestTemplate restTemplate() {
return new RestTemplate();
}
}
Original file line number Diff line number Diff line change
@@ -1,21 +1,29 @@
package com.example.recruitment.controller;

import com.example.recruitment.service.UserClient;
import com.example.recruitment.common.ApiResponse;
import com.example.recruitment.dto.RecruitmentDetailDto;
import com.example.recruitment.dto.RecruitmentRequestDto;
import com.example.recruitment.dto.RecruitmentResponseDto;
import com.example.recruitment.dto.order.OrderRequestDto;
import com.example.recruitment.entity.Recruitment;
import com.example.recruitment.entity.RecruitmentParticipant;
import com.example.recruitment.entity.User;
import com.example.recruitment.entity.Store;
import com.example.recruitment.exception.CustomException;
import com.example.recruitment.exception.ErrorCode;
import com.example.recruitment.repository.RecruitmentParticipantRepository;
import com.example.recruitment.repository.RecruitmentRepository;
import com.example.recruitment.repository.StoreRepository;
import com.example.recruitment.service.RecruitmentService;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@RestController
@RequiredArgsConstructor
Expand All @@ -25,78 +33,90 @@ public class RecruitmentController {
private final RecruitmentService recruitmentService;
private final RecruitmentRepository recruitmentRepository;
private final RecruitmentParticipantRepository participantRepository;
private final StoreRepository storeRepository;
private final UserClient userClient; // 유저 서비스 연동용 Feign Client

//모집글 생성 (Order 서버에 주문 생성 포함)
@PostMapping
public ResponseEntity<?> createRecruitment(@Valid @RequestBody RecruitmentRequestDto dto) {
recruitmentService.createRecruitment(dto);
return ResponseEntity.ok("모집글 생성 완료");
public ResponseEntity<ApiResponse<Map<String, Long>>> createRecruitment(@Valid @RequestBody RecruitmentRequestDto dto) {
Long recruitmentId = recruitmentService.createRecruitment(dto);
return ResponseEntity
.status(HttpStatus.CREATED)
.body(ApiResponse.created(Map.of("recruitmentId", recruitmentId), "모집글 생성 완료"));
}

//모집글 참여 (Order 서버에 주문 생성 포함)
@PostMapping("/{recruitmentId}/join")
public ResponseEntity<?> joinRecruitment(@PathVariable Long recruitmentId,
@RequestParam Long userId,
@RequestBody OrderRequestDto orderRequestDto) {
public ResponseEntity<ApiResponse<String>> joinRecruitment(@PathVariable Long recruitmentId,
@RequestParam Long userId,
@Valid @RequestBody OrderRequestDto orderRequestDto) {
recruitmentService.joinRecruitment(recruitmentId, userId, orderRequestDto);
return ResponseEntity.ok("모집글 참여 완료");
return ResponseEntity.ok(ApiResponse.ok(null, "모집글 참여 완료"));
}

// 모집글 전체 조회
@GetMapping
public List<Recruitment> getAll() {
return recruitmentRepository.findAll();
public ResponseEntity<ApiResponse<List<RecruitmentResponseDto>>> getAll() {
List<Recruitment> recruitments = recruitmentRepository.findAll();
List<RecruitmentResponseDto> response = recruitments.stream()
.map(RecruitmentResponseDto::new)
.toList();
return ResponseEntity.ok(ApiResponse.ok(response, "모든 모집글 조회 성공"));
}

// 상태별 조회
@GetMapping(params = "status")
public List<Recruitment> getByStatus(@RequestParam String status) {
return recruitmentRepository.findByStatus(status);
public ResponseEntity<ApiResponse<List<RecruitmentResponseDto>>> getByStatus(@RequestParam String status) {
List<Recruitment> recruitments = recruitmentRepository.findByStatus(status);
List<RecruitmentResponseDto> response = recruitments.stream()
.map(RecruitmentResponseDto::new)
.toList();
return ResponseEntity.ok(ApiResponse.ok(response, "상태별 모집글 조회 성공"));
}

// 모집글 상세 조회
@GetMapping("/{recruitmentId}")
public ResponseEntity<?> getRecruitmentDetail(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();
public ResponseEntity<ApiResponse<RecruitmentDetailDto>> getRecruitmentDetail(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));

// ✅ 작성자 정보 → 유저 서비스에서 가져옴
RecruitmentDetailDto.UserDto writer = userClient.getUserById(recruitment.getUser().getId());

List<RecruitmentParticipant> participants =
participantRepository.findByRecruitmentId(recruitmentId);
List<User> participantUsers = participants.stream()
.map(RecruitmentParticipant::getUser)
// ✅ 참여자 정보
List<RecruitmentParticipant> participants = participantRepository.findByRecruitmentId(recruitmentId);
List<RecruitmentDetailDto.UserDto> participantUsers = participants.stream()
.map(p -> userClient.getUserById(p.getUser().getId()))
.toList();
Comment on lines +79 to 85
Copy link

Choose a reason for hiding this comment

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

🛠️ Refactor suggestion

UserClient 호출 시 예외 처리 필요

UserClient의 getUserById 메서드는 외부 서비스 호출이므로 실패할 수 있습니다. 특히 반복문 내에서 호출 시 일부 사용자 정보 조회 실패로 전체 요청이 실패할 수 있습니다.

부분 실패를 허용하는 방식으로 개선하거나, 최소한 적절한 예외 처리를 추가하세요:

         List<RecruitmentDetailDto.UserDto> participantUsers = participants.stream()
-                .map(p -> userClient.getUserById(p.getUser().getId()))
+                .map(p -> {
+                    try {
+                        return userClient.getUserById(p.getUser().getId());
+                    } catch (CustomException e) {
+                        // 로그 기록 후 기본값 반환 또는 예외 재발생
+                        return new RecruitmentDetailDto.UserDto(
+                            p.getUser().getId(), 
+                            "사용자 정보 조회 실패", 
+                            null
+                        );
+                    }
+                })
                 .toList();
📝 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
RecruitmentDetailDto.UserDto writer = userClient.getUserById(recruitment.getUser().getId());
List<RecruitmentParticipant> participants =
participantRepository.findByRecruitmentId(recruitmentId);
List<User> participantUsers = participants.stream()
.map(RecruitmentParticipant::getUser)
// ✅ 참여자 정보
List<RecruitmentParticipant> participants = participantRepository.findByRecruitmentId(recruitmentId);
List<RecruitmentDetailDto.UserDto> participantUsers = participants.stream()
.map(p -> userClient.getUserById(p.getUser().getId()))
.toList();
RecruitmentDetailDto.UserDto writer = userClient.getUserById(recruitment.getUser().getId());
// ✅ 참여자 정보
List<RecruitmentParticipant> participants = participantRepository.findByRecruitmentId(recruitmentId);
List<RecruitmentDetailDto.UserDto> participantUsers = participants.stream()
- .map(p -> userClient.getUserById(p.getUser().getId()))
+ .map(p -> {
+ try {
+ return userClient.getUserById(p.getUser().getId());
+ } catch (CustomException e) {
+ // TODO: 로깅 추가
+ return new RecruitmentDetailDto.UserDto(
+ p.getUser().getId(),
+ "사용자 정보 조회 실패",
+ null
+ );
+ }
+ })
.toList();
🤖 Prompt for AI Agents
In
recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.java
around lines 79 to 85, the calls to userClient.getUserById may fail due to
external service issues, especially inside the stream mapping for participants.
To fix this, add exception handling around each getUserById call to catch and
handle failures gracefully, such as returning a default or null user DTO, or
logging the error without failing the entire request. This ensures partial
failures do not cause the whole process to fail.


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);
List<Long> orderIds = participants.stream()
.map(RecruitmentParticipant::getOrderId)
.toList();

RecruitmentDetailDto dto = new RecruitmentDetailDto(recruitment, writer, participantUsers, orderIds);

return ResponseEntity.ok(ApiResponse.ok(dto, "모집 상세 조회 성공"));
}

// 유저가 만든 모집글
@GetMapping("/user/{userId}/created-recruitments")
public List<Recruitment> getRecruitmentsCreatedByUser(@PathVariable Long userId) {
return recruitmentRepository.findByUserId(userId);
public ResponseEntity<ApiResponse<List<RecruitmentResponseDto>>> getRecruitmentsCreatedByUser(@PathVariable Long userId) {
List<Recruitment> list = recruitmentRepository.findByUserId(userId);
List<RecruitmentResponseDto> response = list.stream()
.map(RecruitmentResponseDto::new)
.toList();
return ResponseEntity.ok(ApiResponse.ok(response, "작성한 모집글 조회 성공"));
}

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

// 모집 상태 업데이트
@PatchMapping("/{recruitmentId}/status")
public ResponseEntity<?> updateRecruitmentStatus(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();
public ResponseEntity<ApiResponse<String>> updateRecruitmentStatus(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));

LocalDateTime now = LocalDateTime.now();
long participantCount = participantRepository.countByRecruitmentId(recruitmentId);

Expand All @@ -107,62 +127,70 @@ public ResponseEntity<?> updateRecruitmentStatus(@PathVariable Long recruitmentI
recruitment.setStatus("FAILED");
}
recruitmentRepository.save(recruitment);
return ResponseEntity.ok("상태가 " + recruitment.getStatus() + "로 변경되었습니다.");
return ResponseEntity.ok(ApiResponse.ok(null, "상태가 " + recruitment.getStatus() + "로 변경되었습니다."));
} else {
return ResponseEntity.ok("아직 마감 시간이 지나지 않았습니다.");
return ResponseEntity.ok(ApiResponse.ok(null, "아직 마감 시간이 지나지 않았습니다."));
}
}

// 주문 수락 상태 변경
@PatchMapping("/{recruitmentId}/accept")
public ResponseEntity<?> acceptRecruitment(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();
public ResponseEntity<ApiResponse<String>> acceptRecruitment(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));

if (!"CONFIRMED".equals(recruitment.getStatus())) {
return ResponseEntity.badRequest().body("주문 수락은 CONFIRMED 상태에서만 가능합니다.");
return ResponseEntity.badRequest()
.body(ApiResponse.fail(4001, "주문 수락은 CONFIRMED 상태에서만 가능합니다.", HttpStatus.BAD_REQUEST));
}
recruitment.setStatus("ACCEPTED");
recruitmentRepository.save(recruitment);
return ResponseEntity.ok("상태가 ACCEPTED로 변경되었습니다.");
return ResponseEntity.ok(ApiResponse.ok(null, "상태가 ACCEPTED로 변경되었습니다."));
}

// 배달 완료 상태 변경
@PatchMapping("/{recruitmentId}/deliver")
public ResponseEntity<?> completeDelivery(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();
public ResponseEntity<ApiResponse<String>> completeDelivery(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));

if (!"ACCEPTED".equals(recruitment.getStatus())) {
return ResponseEntity.badRequest().body("배달 완료는 ACCEPTED 상태에서만 가능합니다.");
return ResponseEntity.badRequest()
.body(ApiResponse.fail(4002, "배달 완료는 ACCEPTED 상태에서만 가능합니다.", HttpStatus.BAD_REQUEST));
}
recruitment.setStatus("DELIVERED");
recruitmentRepository.save(recruitment);
return ResponseEntity.ok("상태가 DELIVERED로 변경되었습니다.");
return ResponseEntity.ok(ApiResponse.ok(null, "상태가 DELIVERED로 변경되었습니다."));
}

// 모집글 삭제
@DeleteMapping("/{recruitmentId}")
public ResponseEntity<?> deleteRecruitment(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId).orElseThrow();
public ResponseEntity<ApiResponse<String>> deleteRecruitment(@PathVariable Long recruitmentId) {
Recruitment recruitment = recruitmentRepository.findById(recruitmentId)
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));
recruitmentRepository.delete(recruitment);
return ResponseEntity.ok("모집글이 삭제되었습니다.");
return ResponseEntity.ok(ApiResponse.ok(null, "모집글이 삭제되었습니다."));
}

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

if (!recruitment.getUser().getId().equals(dto.getUserId())) {
return ResponseEntity.status(403).body("작성자만 수정할 수 있습니다.");
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(ApiResponse.fail(4031, "작성자만 수정할 수 있습니다.", HttpStatus.FORBIDDEN));
}

recruitment.setTitle(dto.getTitle());
recruitment.setDescription(dto.getDescription());
recruitment.setDeadlineTime(dto.getDeadlineTime());

if (dto.getStoreId() != null) {
recruitment.setStore(recruitment.getStore());
Store store = storeRepository.findById(dto.getStoreId())
.orElseThrow(() -> new CustomException(ErrorCode.NOT_FOUND));
recruitment.setStore(store);
}

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

import com.example.recruitment.entity.Recruitment;
import com.example.recruitment.entity.Store;
import com.example.recruitment.entity.User;
import lombok.Getter;
import lombok.Setter;

import java.time.LocalDateTime;
import java.util.List;

@Getter @Setter
@Getter
@Setter
public class RecruitmentDetailDto {

private Long id;
private String title;
private String description;
private String status;
private LocalDateTime deadlineTime;
private User user;
private Store store;
private List<User> participants;

private UserDto user;
private StoreDto store;
private List<UserDto> participants;
private List<Long> orderIds;

public RecruitmentDetailDto(
Recruitment recruitment,
UserDto writer,
List<UserDto> participantUsers,
List<Long> orderIds
) {
this.id = recruitment.getId();
this.title = recruitment.getTitle();
this.description = recruitment.getDescription();
this.status = recruitment.getStatus();
this.deadlineTime = recruitment.getDeadlineTime();
this.user = writer; // ✅ 유저 서비스로부터 가져온 작성자 정보
this.store = new StoreDto(recruitment.getStore());
this.participants = participantUsers;
this.orderIds = orderIds;
}

@Getter
@Setter
public static class UserDto {
private Long id;
private String name;
private String email;

public UserDto(Long id, String name, String email) {
this.id = id;
this.name = name;
this.email = email;
}
}

@Getter
@Setter
public static class StoreDto {
private Long id;
private String name;
private String category;
private String location;

public StoreDto(Store store) {
this.id = store.getId();
this.name = store.getName();
this.category = store.getCategory();
this.location = store.getLocation();
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -33,9 +33,11 @@ public class RecruitmentRequestDto {
private String category;

// 주문용 메뉴 정보
@NotNull(message = "메뉴 정보는 필수입니다.")
@Size(min = 1, message = "최소 하나 이상의 메뉴를 선택해야 합니다.")
private List<OrderRequestDto.MenuDto> menus;

//OrderRequestDto 변환 메서드
// OrderRequestDto 변환 메서드
public OrderRequestDto toOrderRequestDto() {
OrderRequestDto dto = new OrderRequestDto();
dto.setUserId(this.userId);
Expand Down
Loading
Loading