Skip to content

Commit e38f8a4

Browse files
authored
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} 추가 및 개행 제거
1 parent 7f24785 commit e38f8a4

File tree

8 files changed

+339
-1
lines changed

8 files changed

+339
-1
lines changed
Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
package in.koreatech.koin.domain.shoptoOrderable.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.shoptoOrderable.dto.ShopToOrderableRequest;
11+
import in.koreatech.koin.global.auth.Auth;
12+
import in.koreatech.koin.global.code.ApiResponseCode;
13+
import in.koreatech.koin.global.code.ApiResponseCodes;
14+
import io.swagger.v3.oas.annotations.Operation;
15+
import io.swagger.v3.oas.annotations.security.SecurityRequirement;
16+
import io.swagger.v3.oas.annotations.tags.Tag;
17+
import jakarta.validation.Valid;
18+
19+
@Tag(name = "(Normal) Shop To Orderable Request: 주문 서비스 가입 요청", description = "사장님이 주문 서비스 가입을 요청하기위한 API")
20+
public interface ShopToOrderableApi {
21+
22+
@ApiResponseCodes({
23+
ApiResponseCode.OK,
24+
ApiResponseCode.ILLEGAL_ARGUMENT,
25+
ApiResponseCode.FORBIDDEN_USER_TYPE,
26+
ApiResponseCode.NOT_FOUND_USER,
27+
ApiResponseCode.NOT_FOUND_SHOP,
28+
})
29+
@Operation(summary = "사장님 주문 서비스 가입 요청")
30+
@SecurityRequirement(name = "Jwt Authentication")
31+
@PostMapping("/owner/shops/{shopId}/orderable-requests")
32+
ResponseEntity<Void> createOrderableRequest(
33+
@Auth(permit = {OWNER}) Integer ownerId,
34+
@PathVariable Integer shopId,
35+
@RequestBody @Valid ShopToOrderableRequest request
36+
);
37+
}
Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
package in.koreatech.koin.domain.shoptoOrderable.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.shoptoOrderable.dto.ShopToOrderableRequest;
10+
import in.koreatech.koin.domain.shoptoOrderable.service.ShopToOrderableService;
11+
import lombok.RequiredArgsConstructor;
12+
13+
import static in.koreatech.koin.domain.user.model.UserType.OWNER;
14+
15+
import org.springframework.web.bind.annotation.RequestBody;
16+
17+
import in.koreatech.koin.global.auth.Auth;
18+
import jakarta.validation.Valid;
19+
20+
//Todo: ShopToOrderable 이라는 명칭은 임시임 추후 변경
21+
@RestController
22+
@RequiredArgsConstructor
23+
public class ShopToOrderableController implements ShopToOrderableApi {
24+
25+
private final ShopToOrderableService shopToOrderableService;
26+
27+
@PostMapping("/owner/shops/{shopId}/orderable-requests")
28+
public ResponseEntity<Void> createOrderableRequest(
29+
@Auth(permit = {OWNER}) Integer ownerId,
30+
@PathVariable Integer shopId,
31+
@RequestBody @Valid ShopToOrderableRequest request
32+
) {
33+
shopToOrderableService.createOrderableRequest(ownerId, request, shopId);
34+
return ResponseEntity.status(HttpStatus.CREATED).build();
35+
}
36+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
package in.koreatech.koin.domain.shoptoOrderable.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 io.swagger.v3.oas.annotations.media.Schema;
9+
import jakarta.validation.constraints.Min;
10+
import jakarta.validation.constraints.NotBlank;
11+
import jakarta.validation.constraints.NotNull;
12+
import jakarta.validation.constraints.Size;
13+
14+
15+
@JsonNaming(SnakeCaseStrategy.class)
16+
public record ShopToOrderableRequest(
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 takeout,
26+
27+
@Schema(description = "배달 옵션 (CAMPUS/OUTSIDE/BOTH)", example = "BOTH", requiredMode = REQUIRED)
28+
@NotNull(message = "배달 옵션은 필수입니다.")
29+
String 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 outsideDeliveryTip,
40+
41+
@Schema(description = "가게 운영 여부", example = "true", requiredMode = REQUIRED)
42+
@NotNull(message = "가게 운영 여부는 필수입니다.")
43+
Boolean isOpen,
44+
45+
@Schema(description = "사업자 등록증 URL", example = "https://example.com/business_license.jpg", requiredMode = REQUIRED)
46+
@NotBlank(message = "사업자 등록증 URL은 필수입니다.")
47+
String businessLicenseUrl,
48+
49+
@Schema(description = "영업 신고증 URL", example = "https://example.com/business_certificate.jpg", requiredMode = REQUIRED)
50+
@NotBlank(message = "영업 신고증 URL은 필수입니다.")
51+
String businessCertificateUrl,
52+
53+
@Schema(description = "통장 사본 URL", example = "https://example.com/bank_copy.jpg", requiredMode = REQUIRED)
54+
@NotBlank(message = "통장 사본 URL은 필수입니다.")
55+
String bankCopyUrl,
56+
57+
@Schema(description = "은행명", example = "국민은행", requiredMode = REQUIRED)
58+
@NotBlank(message = "은행명은 필수입니다.")
59+
@Size(max = 10, message = "은행명은 10자 이하여야 합니다.")
60+
String bank,
61+
62+
@Schema(description = "계좌번호", example = "123-456-789", requiredMode = REQUIRED)
63+
@NotBlank(message = "계좌번호는 필수입니다.")
64+
@Size(max = 20, message = "계좌번호는 20자 이하여야 합니다.")
65+
String accountNumber
66+
) {
67+
}
Lines changed: 121 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,121 @@
1+
package in.koreatech.koin.domain.shoptoOrderable.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.OneToOne;
17+
import jakarta.persistence.Table;
18+
import jakarta.validation.constraints.Size;
19+
20+
import java.time.LocalDateTime;
21+
22+
import lombok.Builder;
23+
import lombok.Getter;
24+
import lombok.NoArgsConstructor;
25+
26+
@Entity
27+
@Getter
28+
@Table(name = "shop_to_orderable")
29+
@NoArgsConstructor(access = PROTECTED)
30+
public class ShopToOrderable extends BaseEntity {
31+
// 임시 필드들 (추후 최종본에선 변경)
32+
@Id
33+
@GeneratedValue(strategy = GenerationType.IDENTITY)
34+
private Integer id;
35+
36+
@OneToOne(fetch = FetchType.LAZY)
37+
@JoinColumn(name = "shop_id", nullable = false)
38+
private Shop shop;
39+
40+
@Column(nullable = false)
41+
private Integer minimumOrderAmount;
42+
43+
@Column(nullable = false)
44+
private Boolean takeout;
45+
46+
@Column(nullable = false)
47+
private String deliveryOption;
48+
49+
@Column(nullable = false)
50+
private Integer campusDeliveryTip;
51+
52+
@Column(nullable = false)
53+
private Integer outsideDeliveryTip;
54+
55+
@Column(nullable = false)
56+
private Boolean isOpen;
57+
58+
@Column(nullable = false)
59+
private String businessLicenseUrl;
60+
61+
@Column(nullable = false)
62+
private String businessCertificateUrl;
63+
64+
@Column(nullable = false)
65+
private String bankCopyUrl;
66+
67+
@Size(max = 10)
68+
@Column(name = "bank", length = 10)
69+
private String bank;
70+
71+
@Size(max = 20)
72+
@Column(name = "account_number", length = 20)
73+
private String accountNumber;
74+
75+
@Enumerated(EnumType.STRING)
76+
@Column(nullable = false)
77+
private ShopToOrderableRequestStatus requestStatus;
78+
79+
@Column(name = "approved_at", nullable = false, updatable = false, columnDefinition = "TIMESTAMP")
80+
private LocalDateTime approvedAt;
81+
82+
@Builder
83+
public ShopToOrderable(
84+
Shop shop,
85+
Integer minimumOrderAmount,
86+
Boolean takeout,
87+
String deliveryOption,
88+
Integer campusDeliveryTip,
89+
Integer outsideDeliveryTip,
90+
Boolean isOpen,
91+
String businessLicenseUrl,
92+
String businessCertificateUrl,
93+
String bankCopyUrl,
94+
String bank,
95+
String accountNumber
96+
) {
97+
this.shop = shop;
98+
this.minimumOrderAmount = minimumOrderAmount;
99+
this.takeout = takeout;
100+
this.deliveryOption = deliveryOption;
101+
this.campusDeliveryTip = campusDeliveryTip;
102+
this.outsideDeliveryTip = outsideDeliveryTip;
103+
this.isOpen = isOpen;
104+
this.requestStatus = ShopToOrderableRequestStatus.PENDING;
105+
this.businessLicenseUrl = businessLicenseUrl;
106+
this.businessCertificateUrl = businessCertificateUrl;
107+
this.bankCopyUrl = bankCopyUrl;
108+
this.bank = bank;
109+
this.accountNumber = accountNumber;
110+
this.approvedAt = null;
111+
}
112+
113+
public void approveRequest() {
114+
this.requestStatus = ShopToOrderableRequestStatus.APPROVED;
115+
this.approvedAt = LocalDateTime.now();
116+
}
117+
118+
public void rejectRequest() {
119+
this.requestStatus = ShopToOrderableRequestStatus.REJECTED;
120+
}
121+
}
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
package in.koreatech.koin.domain.shoptoOrderable.model;
2+
3+
public enum ShopToOrderableRequestStatus {
4+
PENDING, APPROVED, REJECTED
5+
}
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
package in.koreatech.koin.domain.shoptoOrderable.repository;
2+
3+
import in.koreatech.koin.domain.shoptoOrderable.model.ShopToOrderable;
4+
5+
import org.springframework.data.repository.Repository;
6+
7+
import java.util.Optional;
8+
9+
public interface ShopToOrderableRepository extends Repository<ShopToOrderable, Integer> {
10+
11+
ShopToOrderable save(ShopToOrderable shopToOrderable);
12+
13+
Optional<ShopToOrderable> findByShopId(Integer shopId);
14+
15+
boolean existsByShopId(Integer shopId);
16+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package in.koreatech.koin.domain.shoptoOrderable.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.shoptoOrderable.dto.ShopToOrderableRequest;
9+
import in.koreatech.koin.domain.shoptoOrderable.model.ShopToOrderable;
10+
import in.koreatech.koin.domain.shoptoOrderable.repository.ShopToOrderableRepository;
11+
import in.koreatech.koin.global.code.ApiResponseCode;
12+
import lombok.RequiredArgsConstructor;
13+
import in.koreatech.koin.global.exception.CustomException;
14+
15+
@Service
16+
@RequiredArgsConstructor
17+
public class ShopToOrderableService {
18+
19+
private final ShopToOrderableRepository shopToOrderableRepository;
20+
private final ShopRepository shopRepository;
21+
22+
@Transactional
23+
public void createOrderableRequest(Integer ownerId, ShopToOrderableRequest request, Integer shopId) {
24+
Shop shop = shopRepository.findById(shopId)
25+
.orElseThrow(
26+
() -> CustomException.of(ApiResponseCode.NOT_FOUND_SHOP, "shopId: " + shopId));
27+
28+
// 이미 신청한 내역이 있는지 확인
29+
if (shopToOrderableRepository.existsByShopId(shopId)) {
30+
throw CustomException.of(ApiResponseCode.ALREADY_REQUESTED_ORDERABLE_SHOP, "shopId: " + shopId);
31+
}
32+
33+
// 가게 사장님인지 확인
34+
35+
// 이미 주문가능 상점인지 확인
36+
37+
ShopToOrderable shopToOrderable = ShopToOrderable.builder()
38+
.shop(shop)
39+
.minimumOrderAmount(request.minimumOrderAmount())
40+
.takeout(request.takeout())
41+
.deliveryOption(request.deliveryOption())
42+
.campusDeliveryTip(request.campusDeliveryTip())
43+
.outsideDeliveryTip(request.outsideDeliveryTip())
44+
.isOpen(request.isOpen())
45+
.businessLicenseUrl(request.businessLicenseUrl())
46+
.businessCertificateUrl(request.businessCertificateUrl())
47+
.bankCopyUrl(request.bankCopyUrl())
48+
.bank(request.bank())
49+
.accountNumber(request.accountNumber())
50+
.build();
51+
52+
shopToOrderableRepository.save(shopToOrderable);
53+
}
54+
}

src/main/java/in/koreatech/koin/global/code/ApiResponseCode.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,7 @@ public enum ApiResponseCode {
9797
NOT_FOUND_CLUB_RECRUITMENT(HttpStatus.NOT_FOUND, "동아리 모집 공고가 존재하지 않습니다."),
9898
NOT_FOUND_CLUB_EVENT(HttpStatus.NOT_FOUND, "동아리 행사가 존재하지 않습니다."),
9999
NOT_FOUND_DELIVERY_ADDRESS(HttpStatus.NOT_FOUND, "주소가 존재하지 않습니다."),
100-
NOT_FOUND_ORDERABLE_SHOP(HttpStatus.NOT_FOUND, "상점이 존재하지 않습니다."),
100+
NOT_FOUND_ORDERABLE_SHOP(HttpStatus.NOT_FOUND, "주문 가능 상점이 존재하지 않습니다."),
101101
NOT_FOUND_ORDERABLE_SHOP_MENU(HttpStatus.NOT_FOUND, "메뉴가 존재하지 않습니다"),
102102
NOT_FOUND_ORDERABLE_SHOP_MENU_PRICE(HttpStatus.NOT_FOUND, "유효하지 않은 가격 ID 입니다."),
103103
NOT_FOUND_ORDERABLE_SHOP_MENU_OPTION(HttpStatus.NOT_FOUND, "유효하지 않은 옵션 ID 입니다."),
@@ -108,6 +108,7 @@ public enum ApiResponseCode {
108108
NOT_FOUND_TEMPORARY_PAYMENT(HttpStatus.NOT_FOUND, "임시 결제 정보가 존재하지 않습니다."),
109109
NOT_FOUND_PAYMENT(HttpStatus.NOT_FOUND, "결제 정보가 존재하지 않습니다."),
110110
NOT_FOUND_ORDER(HttpStatus.NOT_FOUND, "주문 정보가 존재하지 않습니다."),
111+
NOT_FOUND_SHOP(HttpStatus.NOT_FOUND, "상점이 존재하지 않습니다."),
111112

112113
/**
113114
* 409 CONFLICT (중복 혹은 충돌)
@@ -119,6 +120,7 @@ public enum ApiResponseCode {
119120
REQUEST_TOO_FAST(HttpStatus.CONFLICT, "요청이 너무 빠릅니다. 다시 요청해주세요."),
120121
OPTIMISTIC_LOCKING_FAILURE(HttpStatus.CONFLICT, "이미 처리된 요청입니다."),
121122
DUPLICATE_CLUB_RECRUITMENT(HttpStatus.CONFLICT, "동아리 공고가 이미 존재합니다."),
123+
ALREADY_REQUESTED_ORDERABLE_SHOP(HttpStatus.CONFLICT, "이미 전환 신청이 접수된 상점입니다."),
122124

123125
/**
124126
* 429 Too Many Requests (요청량 초과)

0 commit comments

Comments
 (0)