Skip to content

Commit b6f33ba

Browse files
authored
feat: 주문 상점 전환 POST API를 추가하다 (#2049)
* feat: 주문 가능 상점 전환(주문 서비스 가입) POST API 틀제작 (#2036) * feat: 빈 java 파일들 생성 * feat: ShopToOrderable Post를 스켈레톤코드 작성 * refact: DTO, Model 필드 생성 (임시) * feat: Controller 수정 * feat: Service, Repository 단 * refact: 컨벤션 수정 * refact: 예외코드 추가 및 수정, 엔드포인트 경로 변경, * refact: Entity 리펙토링 * refact: 미사용 import 제거 * refact: {shopId} 추가 및 개행 제거 * feat: 주문 가능 상점 전환(주문 서비스 가입) POST API 검증로직 추가 및 테스트 추가 (#2048) * feat: 검증 추가 * feat: 주문가능상점변환 테스트 추가 * refact: 매개변수 stirng -> ShopToOrderableRequestStatus로 변경 * refact: 예외 명 변경 * feat : flyway 추가 및 컬럼 명 추가 * refact : isOpen 삭제 * fix : isOpen 테스트 부분 삭제 * refact : idx제거 * refact : 형식 준수 * feat : fk_shop_to_orderable_shop 외래키 추가 * fix : approvedAt NULL 불일치 제거 * feat: 상점 @manytoone으로 변경 * feat: ShopToOrderableDeliveryOption 추가 * refact: @NotNull 추가 * refact: 검증로직 메서드화 * refact: 개행제거 * fix : flyway fk 테이블명 불일치 해결 * feat: Swagger 패키지 추가 * refact: takeout -> isTakeout 컬럼명 수정 * refact: outside_delivery_tip -> off_campus_delivery_tip 컬럼명 수정 * feat : 동시성 방지 @DuplicateGuard 추가 * refact : 가게 사장 검증로직 shop 내부 메서드로 이동 * refact: 예외 Static import처리 * refact: 도메인명 ShopOrderServiceRequest로 변경 * refact: ownershop 하위 패키지로 이동 * refact: api 문서화 리펙토링 * refact: swagger 리펙토링, 엔드포인트 리펙토링 * refact: 테이블명 리펙토링 * refact: 대문자 시작 변수네이밍 수정 * refact: 미사용 메서드 제거 * refact: 메소드 네이밍 및 static import * refact: 변수 첫글자 대문자 변경
1 parent 3b88e57 commit b6f33ba

File tree

13 files changed

+575
-1
lines changed

13 files changed

+575
-1
lines changed
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
package in.koreatech.koin.domain.ownershop.controller;
2+
3+
import static in.koreatech.koin.domain.user.model.UserType.OWNER;
4+
5+
import org.springframework.http.ResponseEntity;
6+
import org.springframework.web.bind.annotation.PathVariable;
7+
import org.springframework.web.bind.annotation.PostMapping;
8+
import org.springframework.web.bind.annotation.RequestBody;
9+
10+
import in.koreatech.koin.domain.ownershop.dto.ShopOrderServiceRequestRequest;
11+
import in.koreatech.koin.global.auth.Auth;
12+
13+
import in.koreatech.koin.global.code.ApiResponseCodes;
14+
import in.koreatech.koin.global.duplicate.DuplicateGuard;
15+
import io.swagger.v3.oas.annotations.Operation;
16+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
17+
import io.swagger.v3.oas.annotations.tags.Tag;
18+
import jakarta.validation.Valid;
19+
20+
import static in.koreatech.koin.global.code.ApiResponseCode.*;
21+
22+
@Tag(name = "(Normal) ShopOrderServieRequest: 주문 서비스 요청 (점주 전용)", description = "주문 서비스 요청 관련 API")
23+
public interface ShopOrderServiceRequestApi {
24+
25+
@ApiResponseCodes({
26+
OK,
27+
ILLEGAL_ARGUMENT,
28+
FORBIDDEN_USER_TYPE,
29+
NOT_FOUND_USER,
30+
NOT_FOUND_SHOP,
31+
FORBIDDEN_SHOP_OWNER,
32+
DUPLICATE_REQUESTED_ORDERABLE_SHOP,
33+
DUPLICATE_ORDERABLE_SHOP
34+
})
35+
@Operation(summary = "사장님 주문 서비스 가입 요청", description = """
36+
### 사장님이 자신이 소유한 가게를 주문 서비스 가입요청을 합니다.
37+
- 요청자는 점주(OWNER)만 가능합니다.
38+
- NULL 값이 들어올 수 있는 필드는 없습니다.
39+
- 이미 가입 요청이 진행 중인 가게는 중복으로 요청할 수 없습니다.
40+
- 이미 주문 서비스에 가입된 가게는 중복으로 요청할 수 없습니다.
41+
- DeliveryOption은 CAMPUS, OFF_CAMPUS, BOTH 중 하나여야 합니다.
42+
- (bank이름은 아직 enum처리가 되어있지 않음에 유의해주세요.)
43+
""")
44+
@SecurityRequirement(name = "Jwt Authentication")
45+
@PostMapping("/owner/shops/{shopId}/order-service-requests")
46+
@DuplicateGuard(key = "#ownerId + ':' + #shopId + ':' + #request.toString()", timeoutSeconds = 300)
47+
ResponseEntity<Void> createOrderableRequest(
48+
@Auth(permit = {OWNER}) Integer ownerId,
49+
@PathVariable Integer shopId,
50+
@RequestBody @Valid ShopOrderServiceRequestRequest request
51+
);
52+
}
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package in.koreatech.koin.domain.ownershop.controller;
2+
3+
import org.springframework.http.HttpStatus;
4+
import org.springframework.http.ResponseEntity;
5+
import org.springframework.web.bind.annotation.PathVariable;
6+
import org.springframework.web.bind.annotation.PostMapping;
7+
import org.springframework.web.bind.annotation.RestController;
8+
9+
import in.koreatech.koin.domain.ownershop.dto.ShopOrderServiceRequestRequest;
10+
import in.koreatech.koin.domain.ownershop.service.ShopOrderServiceRequestService;
11+
import in.koreatech.koin.global.duplicate.DuplicateGuard;
12+
import lombok.RequiredArgsConstructor;
13+
14+
import static in.koreatech.koin.domain.user.model.UserType.OWNER;
15+
16+
import org.springframework.web.bind.annotation.RequestBody;
17+
18+
import in.koreatech.koin.global.auth.Auth;
19+
import jakarta.validation.Valid;
20+
21+
@RestController
22+
@RequiredArgsConstructor
23+
public class ShopOrderServiceRequestController implements ShopOrderServiceRequestApi {
24+
25+
private final ShopOrderServiceRequestService shopOrderServiceRequestService;
26+
27+
@PostMapping("/owner/shops/{shopId}/order-service-requests")
28+
@DuplicateGuard(key = "#ownerId + ':' + #shopId + ':' + #request.toString()", timeoutSeconds = 300)
29+
public ResponseEntity<Void> createOrderableRequest(
30+
@Auth(permit = {OWNER}) Integer ownerId,
31+
@PathVariable Integer shopId,
32+
@RequestBody @Valid ShopOrderServiceRequestRequest request
33+
) {
34+
shopOrderServiceRequestService.createOrderableRequest(ownerId, request, shopId);
35+
return ResponseEntity.status(HttpStatus.CREATED).build();
36+
}
37+
}
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package in.koreatech.koin.domain.ownershop.dto;
2+
3+
import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED;
4+
5+
import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy;
6+
import com.fasterxml.jackson.databind.annotation.JsonNaming;
7+
8+
import in.koreatech.koin.domain.ownershop.model.ShopOrderServiceRequestDeliveryOption;
9+
import io.swagger.v3.oas.annotations.media.Schema;
10+
import jakarta.validation.constraints.Min;
11+
import jakarta.validation.constraints.NotBlank;
12+
import jakarta.validation.constraints.NotNull;
13+
import jakarta.validation.constraints.Size;
14+
15+
@JsonNaming(SnakeCaseStrategy.class)
16+
public record ShopOrderServiceRequestRequest(
17+
18+
@Schema(description = "최소 주문 금액", example = "5000", requiredMode = REQUIRED)
19+
@NotNull(message = "최소 주문 금액은 필수입니다.")
20+
@Min(value = 0, message = "최소 주문 금액은 0원 이상이어야 합니다.")
21+
Integer minimumOrderAmount,
22+
23+
@Schema(description = "포장 가능 여부", example = "true", requiredMode = REQUIRED)
24+
@NotNull(message = "포장 가능 여부는 필수입니다.")
25+
Boolean isTakeout,
26+
27+
@Schema(description = "배달 옵션 (CAMPUS/OUTSIDE/BOTH)", example = "BOTH", requiredMode = REQUIRED)
28+
@NotNull(message = "배달 옵션은 필수입니다.")
29+
ShopOrderServiceRequestDeliveryOption deliveryOption,
30+
31+
@Schema(description = "교내 배달팁", example = "1000", requiredMode = REQUIRED)
32+
@NotNull(message = "교내 배달팁은 필수입니다.")
33+
@Min(value = 0, message = "교내 배달팁은 0원 이상이어야 합니다.")
34+
Integer campusDeliveryTip,
35+
36+
@Schema(description = "교외 배달팁", example = "2000", requiredMode = REQUIRED)
37+
@NotNull(message = "교외 배달팁은 필수입니다.")
38+
@Min(value = 0, message = "교외 배달팁은 0원 이상이어야 합니다.")
39+
Integer offCampusDeliveryTip,
40+
41+
@Schema(description = "사업자 등록증 URL", example = "https://example.com/business_license.jpg", requiredMode = REQUIRED)
42+
@NotBlank(message = "사업자 등록증 URL은 필수입니다.")
43+
String businessLicenseUrl,
44+
45+
@Schema(description = "영업 신고증 URL", example = "https://example.com/business_certificate.jpg", requiredMode = REQUIRED)
46+
@NotBlank(message = "영업 신고증 URL은 필수입니다.")
47+
String businessCertificateUrl,
48+
49+
@Schema(description = "통장 사본 URL", example = "https://example.com/bank_copy.jpg", requiredMode = REQUIRED)
50+
@NotBlank(message = "통장 사본 URL은 필수입니다.")
51+
String bankCopyUrl,
52+
53+
@Schema(description = "은행명", example = "국민은행", requiredMode = REQUIRED)
54+
@NotBlank(message = "은행명은 필수입니다.")
55+
@Size(max = 10, message = "은행명은 10자 이하여야 합니다.")
56+
String bank,
57+
58+
@Schema(description = "계좌번호", example = "123-456-789", requiredMode = REQUIRED)
59+
@NotBlank(message = "계좌번호는 필수입니다.")
60+
@Size(max = 20, message = "계좌번호는 20자 이하여야 합니다.")
61+
String accountNumber
62+
) {
63+
}
Lines changed: 128 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,128 @@
1+
package in.koreatech.koin.domain.ownershop.model;
2+
3+
import static lombok.AccessLevel.PROTECTED;
4+
5+
import in.koreatech.koin.common.model.BaseEntity;
6+
import in.koreatech.koin.domain.shop.model.shop.Shop;
7+
import jakarta.persistence.Column;
8+
import jakarta.persistence.Entity;
9+
import jakarta.persistence.EnumType;
10+
import jakarta.persistence.Enumerated;
11+
import jakarta.persistence.FetchType;
12+
import jakarta.persistence.GeneratedValue;
13+
import jakarta.persistence.GenerationType;
14+
import jakarta.persistence.Id;
15+
import jakarta.persistence.JoinColumn;
16+
import jakarta.persistence.ManyToOne;
17+
import jakarta.persistence.Table;
18+
import jakarta.validation.constraints.NotNull;
19+
import jakarta.validation.constraints.Size;
20+
21+
import java.time.LocalDateTime;
22+
23+
import lombok.Builder;
24+
import lombok.Getter;
25+
import lombok.NoArgsConstructor;
26+
27+
import static in.koreatech.koin.domain.ownershop.model.ShopOrderServiceRequestStatus.*;
28+
29+
@Entity
30+
@Getter
31+
@Table(name = "shop_order_service_requests")
32+
@NoArgsConstructor(access = PROTECTED)
33+
public class ShopOrderServiceRequest extends BaseEntity {
34+
35+
@Id
36+
@GeneratedValue(strategy = GenerationType.IDENTITY)
37+
private Integer id;
38+
39+
@NotNull
40+
@ManyToOne(fetch = FetchType.LAZY)
41+
@JoinColumn(name = "shop_id", nullable = false)
42+
private Shop shop;
43+
44+
@NotNull
45+
@Column(name = "minimum_order_amount", nullable = false)
46+
private Integer minimumOrderAmount;
47+
48+
@Column(name = "is_takeout", nullable = false)
49+
private Boolean isTakeout = false;
50+
51+
@NotNull
52+
@Enumerated(EnumType.STRING)
53+
@Column(name = "delivery_option", nullable = false)
54+
private ShopOrderServiceRequestDeliveryOption deliveryOption;
55+
56+
@Column(name = "campus_delivery_tip", nullable = false)
57+
private Integer campusDeliveryTip = 0;
58+
59+
@Column(name = "off_campus_delivery_tip", nullable = false)
60+
private Integer offCampusDeliveryTip = 0;
61+
62+
@NotNull
63+
@Column(name = "business_license_url", nullable = false)
64+
private String businessLicenseUrl;
65+
66+
@NotNull
67+
@Column(name = "business_certificate_url", nullable = false)
68+
private String businessCertificateUrl;
69+
70+
@NotNull
71+
@Column(name = "bank_copy_url", nullable = false)
72+
private String bankCopyUrl;
73+
74+
@NotNull
75+
@Size(max = 10)
76+
@Column(name = "bank", length = 10, nullable = false)
77+
private String bank;
78+
79+
@NotNull
80+
@Size(max = 20)
81+
@Column(name = "account_number", length = 20, nullable = false)
82+
private String accountNumber;
83+
84+
@Enumerated(EnumType.STRING)
85+
@Column(name = "request_status", nullable = false)
86+
private ShopOrderServiceRequestStatus requestStatus = ShopOrderServiceRequestStatus.PENDING;
87+
88+
@Column(name = "approved_at", columnDefinition = "TIMESTAMP")
89+
private LocalDateTime approvedAt = null;
90+
91+
@Builder
92+
public ShopOrderServiceRequest(
93+
Shop shop,
94+
Integer minimumOrderAmount,
95+
Boolean isTakeout,
96+
ShopOrderServiceRequestDeliveryOption deliveryOption,
97+
Integer campusDeliveryTip,
98+
Integer offCampusDeliveryTip,
99+
String businessLicenseUrl,
100+
String businessCertificateUrl,
101+
String bankCopyUrl,
102+
String bank,
103+
String accountNumber
104+
) {
105+
this.shop = shop;
106+
this.minimumOrderAmount = minimumOrderAmount;
107+
this.isTakeout = isTakeout;
108+
this.deliveryOption = deliveryOption;
109+
this.campusDeliveryTip = campusDeliveryTip;
110+
this.offCampusDeliveryTip = offCampusDeliveryTip;
111+
this.businessLicenseUrl = businessLicenseUrl;
112+
this.businessCertificateUrl = businessCertificateUrl;
113+
this.bankCopyUrl = bankCopyUrl;
114+
this.bank = bank;
115+
this.accountNumber = accountNumber;
116+
this.approvedAt = null;
117+
}
118+
119+
public void approve() {
120+
this.requestStatus = APPROVED;
121+
this.approvedAt = LocalDateTime.now();
122+
}
123+
124+
public void reject() {
125+
this.requestStatus = REJECTED;
126+
}
127+
128+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
package in.koreatech.koin.domain.ownershop.model;
2+
3+
public enum ShopOrderServiceRequestDeliveryOption {
4+
CAMPUS,
5+
OFF_CAMPUS,
6+
BOTH
7+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package in.koreatech.koin.domain.ownershop.model;
2+
3+
public enum ShopOrderServiceRequestStatus {
4+
PENDING, APPROVED, REJECTED
5+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
package in.koreatech.koin.domain.ownershop.repository;
2+
3+
import in.koreatech.koin.domain.ownershop.model.ShopOrderServiceRequest;
4+
import in.koreatech.koin.domain.ownershop.model.ShopOrderServiceRequestStatus;
5+
6+
import org.springframework.data.repository.Repository;
7+
8+
public interface ShopOrderServiceRequestRepository extends Repository<ShopOrderServiceRequest, Integer> {
9+
10+
ShopOrderServiceRequest save(ShopOrderServiceRequest shopOrderServiceRequest);
11+
12+
boolean existsByShopIdAndRequestStatus(Integer shopId, ShopOrderServiceRequestStatus requestStatus);
13+
}
Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,66 @@
1+
package in.koreatech.koin.domain.ownershop.service;
2+
3+
import org.springframework.stereotype.Service;
4+
import org.springframework.transaction.annotation.Transactional;
5+
6+
import in.koreatech.koin.domain.shop.model.shop.Shop;
7+
import in.koreatech.koin.domain.shop.repository.shop.ShopRepository;
8+
import in.koreatech.koin.domain.ownershop.dto.ShopOrderServiceRequestRequest;
9+
import in.koreatech.koin.domain.ownershop.model.ShopOrderServiceRequest;
10+
import in.koreatech.koin.domain.ownershop.repository.ShopOrderServiceRequestRepository;
11+
import in.koreatech.koin.global.exception.CustomException;
12+
import lombok.RequiredArgsConstructor;
13+
14+
import static in.koreatech.koin.domain.ownershop.model.ShopOrderServiceRequestStatus.*;
15+
import static in.koreatech.koin.global.code.ApiResponseCode.*;
16+
17+
@Service
18+
@RequiredArgsConstructor
19+
public class ShopOrderServiceRequestService {
20+
21+
private final ShopOrderServiceRequestRepository shopOrderServiceRequestRepository;
22+
private final ShopRepository shopRepository;
23+
24+
@Transactional
25+
public void createOrderableRequest(Integer ownerId, ShopOrderServiceRequestRequest request, Integer shopId) {
26+
Shop shop = shopRepository.getById(shopId);
27+
28+
validateShopOwner(shop, ownerId);
29+
validateDuplicateRequest(shop);
30+
31+
ShopOrderServiceRequest shopOrderServiceRequest = ShopOrderServiceRequest.builder()
32+
.shop(shop)
33+
.minimumOrderAmount(request.minimumOrderAmount())
34+
.isTakeout(request.isTakeout())
35+
.deliveryOption(request.deliveryOption())
36+
.campusDeliveryTip(request.campusDeliveryTip())
37+
.offCampusDeliveryTip(request.offCampusDeliveryTip())
38+
.businessLicenseUrl(request.businessLicenseUrl())
39+
.businessCertificateUrl(request.businessCertificateUrl())
40+
.bankCopyUrl(request.bankCopyUrl())
41+
.bank(request.bank())
42+
.accountNumber(request.accountNumber())
43+
.build();
44+
45+
shopOrderServiceRequestRepository.save(shopOrderServiceRequest);
46+
}
47+
48+
private void validateShopOwner(Shop shop, Integer ownerId) {
49+
// 가게 사장님인지 확인
50+
if (!shop.isOwner(ownerId)) {
51+
throw CustomException.of(FORBIDDEN_SHOP_OWNER, "ownerId: " + ownerId + ", shopId: " + shop.getId());
52+
}
53+
}
54+
55+
private void validateDuplicateRequest(Shop shop) {
56+
// 이미 신청한 내역이 있는지 확인
57+
if (shopOrderServiceRequestRepository.existsByShopIdAndRequestStatus(shop.getId(), PENDING)) {
58+
throw CustomException.of(DUPLICATE_REQUESTED_ORDERABLE_SHOP, "shopId: " + shop.getId());
59+
}
60+
61+
// 이미 주문가능 상점인지 확인
62+
if (shopOrderServiceRequestRepository.existsByShopIdAndRequestStatus(shop.getId(), APPROVED)) {
63+
throw CustomException.of(DUPLICATE_ORDERABLE_SHOP, "shopId: " + shop.getId());
64+
}
65+
}
66+
}

src/main/java/in/koreatech/koin/domain/shop/model/shop/Shop.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -335,4 +335,8 @@ public void cancelDelete() {
335335
public String getFullAddress() {
336336
return String.join(" ", this.address, this.addressDetail);
337337
}
338+
339+
public boolean isOwner(Integer ownerId) {
340+
return this.owner != null && this.owner.getId().equals(ownerId);
341+
}
338342
}

0 commit comments

Comments
 (0)