Skip to content

Commit 8aaecee

Browse files
authored
Merge pull request #41 from Enomdeul/develop
[FEAT] 샘플 삭제 및 오퍼 로직 수정
2 parents e8a9dbb + c2ea34b commit 8aaecee

26 files changed

Lines changed: 244 additions & 275 deletions

src/main/java/com/enomdeul/domain/offer/controller/OfferController.java

Lines changed: 9 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
import com.enomdeul.domain.offer.dto.response.SentOfferRes;
99
import com.enomdeul.domain.offer.service.OfferService;
1010
import com.enomdeul.global.common.response.ApiResponse;
11+
import jakarta.validation.Valid;
1112
import lombok.RequiredArgsConstructor;
1213
import org.springframework.security.core.annotation.AuthenticationPrincipal;
1314
import org.springframework.web.bind.annotation.*;
@@ -21,7 +22,7 @@ public class OfferController {
2122

2223
private final OfferService offerService;
2324

24-
// 1. 오퍼 전송
25+
// 오퍼 전송
2526
@PostMapping
2627
public ApiResponse<OfferResponse> sendOffer(
2728
@AuthenticationPrincipal String userId,
@@ -32,15 +33,15 @@ public ApiResponse<OfferResponse> sendOffer(
3233
return ApiResponse.onSuccess(response);
3334
}
3435

35-
// 2. 받은 오퍼 조회
36+
// 받은 오퍼 조회
3637
@GetMapping("/received")
3738
public ApiResponse<List<ReceivedOfferRes>> getReceivedOffers(@AuthenticationPrincipal String userId) {
3839
Long receiverId = Long.parseLong(userId);
3940
List<ReceivedOfferRes> response = offerService.getReceivedOffers(receiverId);
4041
return ApiResponse.onSuccess(response);
4142
}
4243

43-
// 3. 보낸 오퍼 조회
44+
// 보낸 오퍼 조회
4445
@GetMapping("/sent")
4546
public ApiResponse<List<SentOfferRes>> getSentOffers(@AuthenticationPrincipal String userId) {
4647
Long senderId = Long.parseLong(userId);
@@ -49,21 +50,17 @@ public ApiResponse<List<SentOfferRes>> getSentOffers(@AuthenticationPrincipal St
4950
}
5051

5152
// 받은 매칭 오퍼 수락/거부 API
52-
// URL: /api/v1/offers/users/{senderId}/status
5353
@PatchMapping("/users/{senderId}/status")
5454
public ApiResponse<OfferAcceptRes> updateOfferStatus(
55-
@AuthenticationPrincipal String userId,
56-
@PathVariable Long senderId,
57-
@RequestBody OfferStatusReq req
55+
@AuthenticationPrincipal String userId, // 내 ID (Receiver)
56+
@PathVariable Long senderId, // 상대방 ID (Sender)
57+
@RequestBody @Valid OfferStatusReq req
5858
) {
5959
Long receiverId = Long.parseLong(userId);
6060

6161
OfferAcceptRes result = offerService.updateOfferStatus(receiverId, senderId, req);
6262

63-
if (result != null) {
64-
return ApiResponse.onSuccess(result);
65-
} else {
66-
return ApiResponse.onSuccess(null);
67-
}
63+
// 수락 시 결과 반환, 거절 시 null 반환 (성공 메시지는 동일)
64+
return ApiResponse.onSuccess(result);
6865
}
6966
}
Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
package com.enomdeul.domain.offer.controller.docs;
2+
3+
import com.enomdeul.domain.offer.dto.request.OfferRequest;
4+
import com.enomdeul.domain.offer.dto.response.OfferResponse;
5+
import com.enomdeul.domain.offer.dto.response.ReceivedOfferRes;
6+
import com.enomdeul.domain.offer.dto.response.SentOfferRes;
7+
import com.enomdeul.domain.offer.exception.OfferErrorCode;
8+
import com.enomdeul.domain.skill.exception.SkillErrorCode;
9+
import com.enomdeul.domain.user.exception.UserErrorCode;
10+
import com.enomdeul.global.common.response.ApiResponse;
11+
import com.enomdeul.global.common.response.code.ApiErrorCode;
12+
import com.enomdeul.global.exception.swagger.ApiErrorCodeExamples;
13+
import io.swagger.v3.oas.annotations.Operation;
14+
import io.swagger.v3.oas.annotations.tags.Tag;
15+
import org.springframework.security.core.annotation.AuthenticationPrincipal;
16+
import org.springframework.web.bind.annotation.RequestBody;
17+
18+
import java.util.List;
19+
20+
@Tag(name = "Offer", description = "오퍼, 매칭 관련 API")
21+
public interface OfferControllerDocs {
22+
23+
@Operation(summary = "오퍼 생성", description = "오퍼를 생성합니다.")
24+
@ApiErrorCodeExamples(
25+
value = {OfferErrorCode.class, UserErrorCode.class, ApiErrorCode.class},
26+
include = { "INTERNAL_SERVER_ERROR", "FORBIDDEN_ERROR", "OFFER_NOT_FOUND", "USER_NOT_FOUND"})
27+
ApiResponse<OfferResponse> sendOffer(@AuthenticationPrincipal String userId, @RequestBody OfferRequest request);
28+
29+
@Operation(summary = "받은 오퍼 목록 조회", description = "받은 오퍼 목록을 조회합니다.")
30+
@ApiErrorCodeExamples(
31+
value = {SkillErrorCode.class, UserErrorCode.class, ApiErrorCode.class},
32+
include = { "INTERNAL_SERVER_ERROR", "FORBIDDEN_ERROR"})
33+
ApiResponse<List<ReceivedOfferRes>> getReceivedOffers(@AuthenticationPrincipal String userId);
34+
35+
@Operation(summary = "보낸 오퍼 목록 조회", description = "보낸 오퍼 목록을 조회합니다.")
36+
@ApiErrorCodeExamples(
37+
value = {ApiErrorCode.class},
38+
include = { "INTERNAL_SERVER_ERROR", "FORBIDDEN_ERROR"})
39+
public ApiResponse<List<SentOfferRes>> getSentOffers(@AuthenticationPrincipal String userId);
40+
41+
}
Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,13 @@
11
package com.enomdeul.domain.offer.dto.request;
22

3+
import jakarta.validation.constraints.NotNull;
34
import lombok.Getter;
45
import lombok.NoArgsConstructor;
56

67
@Getter
78
@NoArgsConstructor
89
public class OfferRequest {
9-
private Long offereeId; // 오퍼를 받을 상대방 ID
10+
11+
@NotNull(message = "상대방 ID는 필수 입력값입니다.") // Long 타입은 @NotNull 사용
12+
private Long offereeId;
1013
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,14 @@
11
package com.enomdeul.domain.offer.dto.request;
22

33
import com.enomdeul.domain.offer.entity.OfferStatus;
4+
import jakarta.validation.constraints.NotNull;
45
import lombok.Getter;
56
import lombok.NoArgsConstructor;
67

78
@Getter
89
@NoArgsConstructor
910
public class OfferStatusReq {
11+
12+
@NotNull(message = "변경할 상태(ACCEPTED/REJECTED)는 필수입니다.")
1013
private OfferStatus status;
1114
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.enomdeul.domain.offer.exception;
2+
3+
import com.enomdeul.global.common.response.code.BaseErrorCode;
4+
import lombok.AllArgsConstructor;
5+
import lombok.Getter;
6+
import org.springframework.http.HttpStatus;
7+
8+
@Getter
9+
@AllArgsConstructor
10+
public enum OfferErrorCode implements BaseErrorCode {
11+
// 오퍼 관련 비즈니스 예외
12+
OFFER_NOT_FOUND(HttpStatus.NOT_FOUND, "OFFER_003", "해당 매칭을 찾을 수 없습니다."),
13+
ALREADY_SENT_OFFER(HttpStatus.BAD_REQUEST, "OFFER_004", "이미 오퍼를 보낸 상대입니다."),
14+
CANNOT_OFFER_SELF(HttpStatus.BAD_REQUEST, "OFFER_005", "자기 자신에게는 오퍼를 보낼 수 없습니다."),
15+
ALREADY_PROCESSED_OFFER(HttpStatus.BAD_REQUEST, "OFFER_006", "이미 수락하거나 거절한 오퍼입니다.");
16+
17+
private final HttpStatus status;
18+
private final String code;
19+
private final String message;
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.enomdeul.domain.offer.exception;
2+
3+
import com.enomdeul.global.common.response.code.BaseErrorCode;
4+
import com.enomdeul.global.exception.CustomException;
5+
import lombok.Getter;
6+
7+
@Getter
8+
public class OfferException extends RuntimeException {
9+
private final BaseErrorCode errorCode;
10+
11+
public OfferException(BaseErrorCode errorCode) {
12+
super(errorCode.getMessage());
13+
this.errorCode = errorCode;
14+
}
15+
}

src/main/java/com/enomdeul/domain/offer/repository/OfferRepository.java

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22

33
import com.enomdeul.domain.offer.entity.Offer;
44
import com.enomdeul.domain.offer.entity.OfferId;
5+
import com.enomdeul.domain.user.entity.User;
56
import org.springframework.data.jpa.repository.JpaRepository;
67
import org.springframework.data.jpa.repository.Query;
78
import org.springframework.data.repository.query.Param;
@@ -10,9 +11,14 @@
1011

1112
public interface OfferRepository extends JpaRepository<Offer, OfferId> {
1213

14+
// 1. 내가(receiverId) 받은 오퍼 목록 조회 (Fetch Join: Offerer)
1315
@Query("SELECT o FROM Offer o JOIN FETCH o.offerer WHERE o.offeree.userId = :receiverId")
1416
List<Offer> findAllReceivedOffers(@Param("receiverId") Long receiverId);
1517

18+
// 2. 내가(senderId) 보낸 오퍼 목록 조회 (Fetch Join: Offeree)
1619
@Query("SELECT o FROM Offer o JOIN FETCH o.offeree WHERE o.offerer.userId = :senderId")
1720
List<Offer> findAllSentOffers(@Param("senderId") Long senderId);
21+
22+
// 3. 중복 오퍼 확인 (보낸사람, 받는사람 조합이 이미 있는지)
23+
boolean existsByOffererAndOfferee(User offerer, User offeree);
1824
}

src/main/java/com/enomdeul/domain/offer/service/OfferService.java

Lines changed: 75 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,9 @@
1111
import com.enomdeul.domain.offer.entity.OfferStatus;
1212
import com.enomdeul.domain.offer.repository.OfferRepository;
1313
import com.enomdeul.domain.user.entity.User;
14+
import com.enomdeul.domain.user.exception.UserErrorCode;
15+
import com.enomdeul.domain.user.exception.UserException;
1416
import com.enomdeul.domain.user.repository.UserRepository;
15-
import com.enomdeul.global.exception.GlobalErrorCode;
16-
import com.enomdeul.global.exception.GlobalException;
1717
import lombok.RequiredArgsConstructor;
1818
import org.springframework.stereotype.Service;
1919
import org.springframework.transaction.annotation.Transactional;
@@ -29,14 +29,30 @@ public class OfferService {
2929
private final OfferRepository offerRepository;
3030
private final UserRepository userRepository;
3131

32+
/**
33+
* 1. 오퍼 전송 (보내기)
34+
*/
3235
@Transactional
3336
public OfferResponse sendOffer(Long senderId, OfferRequest request) {
37+
// 1-1. 자기 자신에게 보내는지 확인
38+
if (senderId.equals(request.getOffereeId())) {
39+
throw new OfferException(OfferErrorCode.CANNOT_OFFER_SELF);
40+
}
41+
42+
// 1-2. 보낸 사람(나) 존재 확인
3443
User sender = userRepository.findById(senderId)
35-
.orElseThrow(() -> new GlobalException(GlobalErrorCode.MEMBER_NOT_FOUND_ERROR));
44+
.orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND));
3645

46+
// 1-3. 받는 사람(상대방) 존재 확인
3747
User receiver = userRepository.findById(request.getOffereeId())
38-
.orElseThrow(() -> new GlobalException(GlobalErrorCode.MEMBER_NOT_FOUND_ERROR));
48+
.orElseThrow(() -> new UserException(UserErrorCode.USER_NOT_FOUND));
3949

50+
// 1-4. 이미 보낸 오퍼인지 확인 (중복 방지)
51+
if (offerRepository.existsByOffererAndOfferee(sender, receiver)) {
52+
throw new OfferException(OfferErrorCode.ALREADY_SENT_OFFER);
53+
}
54+
55+
// 1-5. 오퍼 생성 및 저장 (초기 상태: WAITING)
4056
Offer offer = Offer.builder()
4157
.offerer(sender)
4258
.offeree(receiver)
@@ -47,36 +63,80 @@ public OfferResponse sendOffer(Long senderId, OfferRequest request) {
4763
return OfferResponse.from(offer);
4864
}
4965

66+
/**
67+
* 2. 오퍼 상태 변경 (수락/거절)
68+
*/
5069
@Transactional
51-
public OfferAcceptRes updateOfferStatus(Long receiverId, Long offererId, OfferStatusReq req) {
52-
// 1. 복합키로 오퍼 조회 (보낸 사람 offererId, 받는 사람 receiverId)
53-
OfferId offerId = new OfferId(offererId, receiverId);
70+
public OfferAcceptRes updateOfferStatus(Long receiverId, Long senderId, OfferStatusReq req) {
71+
// 2-1. 유저 존재 확인 (나, 상대방 모두 확인)
72+
if (!userRepository.existsById(receiverId) || !userRepository.existsById(senderId)) {
73+
throw new UserException(UserErrorCode.USER_NOT_FOUND);
74+
}
5475

76+
// 2-2. 오퍼 존재 확인 (복합키 사용)
77+
OfferId offerId = new OfferId(senderId, receiverId);
5578
Offer offer = offerRepository.findById(offerId)
56-
.orElseThrow(() -> new GlobalException(GlobalErrorCode.OFFER_NOT_FOUND));
79+
.orElseThrow(() -> new OfferException(OfferErrorCode.OFFER_NOT_FOUND));
5780

58-
// 2. 상태 변경 (Dirty Checking)
81+
// 2-3. 이미 처리된 오퍼인지 확인 (WAITING이 아니면 예외)
82+
if (offer.getOfferStatus() != OfferStatus.WAITING) {
83+
throw new OfferException(OfferErrorCode.ALREADY_PROCESSED_OFFER);
84+
}
85+
86+
// 2-4. 상태 변경 (Dirty Checking)
5987
offer.changeStatus(req.getStatus());
6088

61-
// 3. 수락(ACCEPTED)인 경우에만 상대방 이메일 반환
89+
// 2-5. 수락(ACCEPTED)인 경우에만 이메일 반환
6290
if (req.getStatus() == OfferStatus.ACCEPTED) {
6391
return OfferAcceptRes.builder()
6492
.email(offer.getOfferer().getEmail())
6593
.build();
6694
}
6795

68-
return null;
96+
return null; // 거절 시 null 반환
6997
}
7098

99+
/**
100+
* 3. 받은 오퍼 목록 조회
101+
*/
71102
public List<ReceivedOfferRes> getReceivedOffers(Long receiverId) {
103+
// 3-1. 유저 존재 확인
104+
if (!userRepository.existsById(receiverId)) {
105+
throw new UserException(UserErrorCode.USER_NOT_FOUND);
106+
}
107+
108+
// 3-2. 목록 조회
72109
List<Offer> offers = offerRepository.findAllReceivedOffers(receiverId);
73-
if (offers.isEmpty()) throw new GlobalException(GlobalErrorCode.OFFER_LIST_EMPTY);
74-
return offers.stream().map(ReceivedOfferRes::from).collect(Collectors.toList());
110+
111+
// 3-3. 목록 없음 예외 처리
112+
if (offers.isEmpty()) {
113+
throw new GlobalException(GlobalErrorCode.OFFER_LIST_EMPTY);
114+
}
115+
116+
return offers.stream()
117+
.map(ReceivedOfferRes::from)
118+
.collect(Collectors.toList());
75119
}
76120

121+
/**
122+
* 4. 보낸 오퍼 목록 조회
123+
*/
77124
public List<SentOfferRes> getSentOffers(Long senderId) {
125+
// 4-1. 유저 존재 확인
126+
if (!userRepository.existsById(senderId)) {
127+
throw new UserException(UserErrorCode.USER_NOT_FOUND);
128+
}
129+
130+
// 4-2. 목록 조회
78131
List<Offer> offers = offerRepository.findAllSentOffers(senderId);
79-
if (offers.isEmpty()) throw new GlobalException(GlobalErrorCode.SENT_OFFER_LIST_EMPTY);
80-
return offers.stream().map(SentOfferRes::from).collect(Collectors.toList());
132+
133+
// 4-3. 목록 없음 예외 처리
134+
if (offers.isEmpty()) {
135+
throw new GlobalException(GlobalErrorCode.SENT_OFFER_LIST_EMPTY);
136+
}
137+
138+
return offers.stream()
139+
.map(SentOfferRes::from)
140+
.collect(Collectors.toList());
81141
}
82142
}

src/main/java/com/enomdeul/domain/sample/controller/SampleController.java

Lines changed: 0 additions & 27 deletions
This file was deleted.

src/main/java/com/enomdeul/domain/sample/controller/docs/SampleControllerDocs.java

Lines changed: 0 additions & 21 deletions
This file was deleted.

0 commit comments

Comments
 (0)