-
Notifications
You must be signed in to change notification settings - Fork 0
feat: 유저 정보 유저 서비스에서 조회하도록 수정 #55
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: recruitment-service/main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| 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 | ||
|
|
@@ -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(); | ||
|
|
||
| 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); | ||
|
|
||
|
|
@@ -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로 변경되었습니다.")); | ||
| } | ||
|
Comment on lines
136
to
162
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 💡 Verification agent 🧩 Analysis chain에러 코드 체계를 확인해 주세요. 현재 사용된 에러 코드(4001, 4002)가 프로젝트의 에러 코드 체계와 일치하는지 확인이 필요합니다. ErrorCode enum에 정의된 에러 코드를 확인해 보겠습니다: 또한 상태 전환 검증 로직의 중복을 줄이기 위해 서비스 레이어에서 처리하는 것을 고려해보세요. 🏁 Script executed: #!/bin/bash
# ErrorCode enum 파일 찾기 및 내용 확인
fd -e java "ErrorCode" -x cat {} \;
# ApiResponse.fail 메서드의 시그니처 확인
ast-grep --pattern 'class ApiResponse {
$$$
fail($$$) {
$$$
}
$$$
}'Length of output: 675 Error 코드 일관성 확인 및 중복 로직 개선 필요 현재 컨트롤러에서 사용 중인 에러 코드(4001, 4002)는
🤖 Prompt for AI Agents |
||
|
|
||
| // 모집글 삭제 | ||
| @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(); | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
💡 Verification agent
🧩 Analysis chain
외부 서비스 호출에 대한 오류 처리가 필요합니다.
현재 구현에는 몇 가지 중요한 문제가 있습니다:
userClient.getUserById()호출이 실패할 경우에 대한 예외 처리가 없습니다.다음과 같은 개선사항을 제안합니다:
UserClient에 대량 조회 API가 있는지 확인해 보겠습니다:
🏁 Script executed:
Length of output: 202
🏁 Script executed:
Length of output: 145
오류 처리 및 N+1 문제 완화 적용 필요
다음 두 가지 사항을 반드시 반영해주세요:
CustomException(ErrorCode.EXTERNAL_SERVICE_ERROR)로 예외 처리UserClient에getUsersByIds(List<Long> ids)메서드 추가를 검토 또는parallelStream())과 개별 try-catch로 호출하여 전체 처리 중 한 건 실패 시에도 다른 호출이 영향을 받지 않도록 방어 코드 삽입수정 예시:
추가로
UserClient인터페이스에 다음 메서드 추가를 검토해주세요:recruitment-service/src/main/java/com/example/recruitment/controller/RecruitmentController.javarecruitment-service/src/main/java/com/example/recruitment/service/UserClient.java🤖 Prompt for AI Agents