-
Notifications
You must be signed in to change notification settings - Fork 1
feat: 주문 상점 전환 POST API를 추가하다 #2049
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: develop
Are you sure you want to change the base?
Changes from all commits
e38f8a4
c8e0d7e
1c2c021
b93a413
4c79e3c
a5e9b71
813e8d7
e8286ba
e2613fc
c713291
a45169e
b67f462
dee35e5
ad017d3
697929e
769d069
14d3471
67d540c
683f121
7755783
de843fd
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,39 @@ | ||
| package in.koreatech.koin.domain.shoptoOrderable.controller; | ||
|
|
||
| import static in.koreatech.koin.domain.user.model.UserType.OWNER; | ||
|
|
||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RequestBody; | ||
|
|
||
| import in.koreatech.koin.domain.shoptoOrderable.dto.ShopToOrderableRequest; | ||
| import in.koreatech.koin.global.auth.Auth; | ||
| import in.koreatech.koin.global.code.ApiResponseCode; | ||
| import in.koreatech.koin.global.code.ApiResponseCodes; | ||
| import in.koreatech.koin.global.duplicate.DuplicateGuard; | ||
| import io.swagger.v3.oas.annotations.Operation; | ||
| import io.swagger.v3.oas.annotations.security.SecurityRequirement; | ||
| import io.swagger.v3.oas.annotations.tags.Tag; | ||
| import jakarta.validation.Valid; | ||
|
|
||
| @Tag(name = "(Normal) Shop To Orderable Request: 주문 서비스 가입 요청", description = "사장님이 주문 서비스 가입을 요청하기위한 API") | ||
| public interface ShopToOrderableApi { | ||
|
|
||
| @ApiResponseCodes({ | ||
| ApiResponseCode.OK, | ||
| ApiResponseCode.ILLEGAL_ARGUMENT, | ||
| ApiResponseCode.FORBIDDEN_USER_TYPE, | ||
| ApiResponseCode.NOT_FOUND_USER, | ||
| ApiResponseCode.NOT_FOUND_SHOP, | ||
| }) | ||
| @Operation(summary = "사장님 주문 서비스 가입 요청") | ||
| @SecurityRequirement(name = "Jwt Authentication") | ||
| @PostMapping("/owner/shops/{shopId}/orderable-requests") | ||
| @DuplicateGuard(key = "#ownerId + ':' + #shopId + ':' + #request.toString()", timeoutSeconds = 300) | ||
| ResponseEntity<Void> createOrderableRequest( | ||
| @Auth(permit = {OWNER}) Integer ownerId, | ||
| @PathVariable Integer shopId, | ||
| @RequestBody @Valid ShopToOrderableRequest request | ||
| ); | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| package in.koreatech.koin.domain.shoptoOrderable.controller; | ||
|
|
||
| import org.springframework.http.HttpStatus; | ||
| import org.springframework.http.ResponseEntity; | ||
| import org.springframework.web.bind.annotation.PathVariable; | ||
| import org.springframework.web.bind.annotation.PostMapping; | ||
| import org.springframework.web.bind.annotation.RestController; | ||
|
|
||
| import in.koreatech.koin.domain.shoptoOrderable.dto.ShopToOrderableRequest; | ||
| import in.koreatech.koin.domain.shoptoOrderable.service.ShopToOrderableService; | ||
| import in.koreatech.koin.global.duplicate.DuplicateGuard; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| import static in.koreatech.koin.domain.user.model.UserType.OWNER; | ||
|
|
||
| import org.springframework.web.bind.annotation.RequestBody; | ||
|
|
||
| import in.koreatech.koin.global.auth.Auth; | ||
| import jakarta.validation.Valid; | ||
|
|
||
| //Todo: ShopToOrderable 이라는 명칭은 임시임 추후 변경 | ||
| @RestController | ||
| @RequiredArgsConstructor | ||
| public class ShopToOrderableController implements ShopToOrderableApi { | ||
|
|
||
| private final ShopToOrderableService shopToOrderableService; | ||
|
|
||
| @PostMapping("/owner/shops/{shopId}/orderable-requests") | ||
| @DuplicateGuard(key = "#ownerId + ':' + #shopId + ':' + #request.toString()", timeoutSeconds = 300) | ||
| public ResponseEntity<Void> createOrderableRequest( | ||
| @Auth(permit = {OWNER}) Integer ownerId, | ||
| @PathVariable Integer shopId, | ||
| @RequestBody @Valid ShopToOrderableRequest request | ||
| ) { | ||
| shopToOrderableService.createOrderableRequest(ownerId, request, shopId); | ||
| return ResponseEntity.status(HttpStatus.CREATED).build(); | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,63 @@ | ||
| package in.koreatech.koin.domain.shoptoOrderable.dto; | ||
|
|
||
| import static io.swagger.v3.oas.annotations.media.Schema.RequiredMode.REQUIRED; | ||
|
|
||
| import com.fasterxml.jackson.databind.PropertyNamingStrategies.SnakeCaseStrategy; | ||
| import com.fasterxml.jackson.databind.annotation.JsonNaming; | ||
|
|
||
| import in.koreatech.koin.domain.shoptoOrderable.model.ShopToOrderableDeliveryOption; | ||
| import io.swagger.v3.oas.annotations.media.Schema; | ||
| import jakarta.validation.constraints.Min; | ||
| import jakarta.validation.constraints.NotBlank; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Size; | ||
|
|
||
asa9874 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| @JsonNaming(SnakeCaseStrategy.class) | ||
| public record ShopToOrderableRequest( | ||
|
|
||
| @Schema(description = "최소 주문 금액", example = "5000", requiredMode = REQUIRED) | ||
| @NotNull(message = "최소 주문 금액은 필수입니다.") | ||
| @Min(value = 0, message = "최소 주문 금액은 0원 이상이어야 합니다.") | ||
| Integer minimumOrderAmount, | ||
|
|
||
| @Schema(description = "포장 가능 여부", example = "true", requiredMode = REQUIRED) | ||
| @NotNull(message = "포장 가능 여부는 필수입니다.") | ||
| Boolean isTakeout, | ||
|
|
||
| @Schema(description = "배달 옵션 (CAMPUS/OUTSIDE/BOTH)", example = "BOTH", requiredMode = REQUIRED) | ||
| @NotNull(message = "배달 옵션은 필수입니다.") | ||
| ShopToOrderableDeliveryOption deliveryOption, | ||
|
|
||
| @Schema(description = "교내 배달팁", example = "1000", requiredMode = REQUIRED) | ||
| @NotNull(message = "교내 배달팁은 필수입니다.") | ||
| @Min(value = 0, message = "교내 배달팁은 0원 이상이어야 합니다.") | ||
| Integer campusDeliveryTip, | ||
|
|
||
| @Schema(description = "교외 배달팁", example = "2000", requiredMode = REQUIRED) | ||
| @NotNull(message = "교외 배달팁은 필수입니다.") | ||
| @Min(value = 0, message = "교외 배달팁은 0원 이상이어야 합니다.") | ||
| Integer offCampusDeliveryTip, | ||
|
|
||
| @Schema(description = "사업자 등록증 URL", example = "https://example.com/business_license.jpg", requiredMode = REQUIRED) | ||
| @NotBlank(message = "사업자 등록증 URL은 필수입니다.") | ||
| String businessLicenseUrl, | ||
|
|
||
| @Schema(description = "영업 신고증 URL", example = "https://example.com/business_certificate.jpg", requiredMode = REQUIRED) | ||
| @NotBlank(message = "영업 신고증 URL은 필수입니다.") | ||
| String businessCertificateUrl, | ||
|
|
||
| @Schema(description = "통장 사본 URL", example = "https://example.com/bank_copy.jpg", requiredMode = REQUIRED) | ||
| @NotBlank(message = "통장 사본 URL은 필수입니다.") | ||
| String bankCopyUrl, | ||
|
|
||
| @Schema(description = "은행명", example = "국민은행", requiredMode = REQUIRED) | ||
| @NotBlank(message = "은행명은 필수입니다.") | ||
| @Size(max = 10, message = "은행명은 10자 이하여야 합니다.") | ||
| String bank, | ||
|
|
||
| @Schema(description = "계좌번호", example = "123-456-789", requiredMode = REQUIRED) | ||
| @NotBlank(message = "계좌번호는 필수입니다.") | ||
| @Size(max = 20, message = "계좌번호는 20자 이하여야 합니다.") | ||
| String accountNumber | ||
| ) { | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,125 @@ | ||
| package in.koreatech.koin.domain.shoptoOrderable.model; | ||
|
|
||
| import static lombok.AccessLevel.PROTECTED; | ||
|
|
||
| import in.koreatech.koin.common.model.BaseEntity; | ||
| import in.koreatech.koin.domain.shop.model.shop.Shop; | ||
| import jakarta.persistence.Column; | ||
| import jakarta.persistence.Entity; | ||
| import jakarta.persistence.EnumType; | ||
| import jakarta.persistence.Enumerated; | ||
| import jakarta.persistence.FetchType; | ||
| import jakarta.persistence.GeneratedValue; | ||
| import jakarta.persistence.GenerationType; | ||
| import jakarta.persistence.Id; | ||
| import jakarta.persistence.JoinColumn; | ||
| import jakarta.persistence.ManyToOne; | ||
| import jakarta.persistence.Table; | ||
| import jakarta.validation.constraints.NotNull; | ||
| import jakarta.validation.constraints.Size; | ||
|
|
||
| import java.time.LocalDateTime; | ||
|
|
||
| import lombok.Builder; | ||
| import lombok.Getter; | ||
| import lombok.NoArgsConstructor; | ||
|
|
||
| @Entity | ||
| @Getter | ||
| @Table(name = "shop_to_orderable") | ||
| @NoArgsConstructor(access = PROTECTED) | ||
| public class ShopToOrderable extends BaseEntity { | ||
|
|
||
| @Id | ||
| @GeneratedValue(strategy = GenerationType.IDENTITY) | ||
| private Integer id; | ||
|
|
||
| @NotNull | ||
| @ManyToOne(fetch = FetchType.LAZY) | ||
| @JoinColumn(name = "shop_id", nullable = false) | ||
| private Shop shop; | ||
|
|
||
| @NotNull | ||
| @Column(name = "minimum_order_amount", nullable = false) | ||
| private Integer minimumOrderAmount; | ||
|
|
||
| @Column(name = "is_takeout", nullable = false) | ||
| private Boolean isTakeout = false; | ||
|
|
||
| @NotNull | ||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "delivery_option", nullable = false) | ||
| private ShopToOrderableDeliveryOption deliveryOption; | ||
|
|
||
| @Column(name = "campus_delivery_tip", nullable = false) | ||
| private Integer campusDeliveryTip = 0; | ||
|
|
||
| @Column(name = "off_campus_delivery_tip", nullable = false) | ||
| private Integer offCampusDeliveryTip = 0; | ||
|
|
||
| @NotNull | ||
| @Column(name = "business_license_url", nullable = false) | ||
| private String businessLicenseUrl; | ||
|
|
||
| @NotNull | ||
| @Column(name = "business_certificate_url", nullable = false) | ||
| private String businessCertificateUrl; | ||
|
|
||
| @NotNull | ||
| @Column(name = "bank_copy_url", nullable = false) | ||
| private String bankCopyUrl; | ||
|
|
||
| @NotNull | ||
| @Size(max = 10) | ||
| @Column(name = "bank", length = 10, nullable = false) | ||
| private String bank; | ||
|
Comment on lines
+73
to
+75
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. Abank를 String 말고 enum으로 관리할 수 있을까요 👀 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. 해당 부분은 요구사항측에서 은행의 종류같은게 정해지면 enum으로 관리 하는게 좋을거같아요. db에서 관리는 enum 말고 테이블로 (1, "OO은행") , (2,"AA은행")으로해서 연관으로 관리한다는 의미인가요? 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.
맞습니당 |
||
|
|
||
| @NotNull | ||
| @Size(max = 20) | ||
| @Column(name = "account_number", length = 20, nullable = false) | ||
| private String accountNumber; | ||
|
|
||
| @Enumerated(EnumType.STRING) | ||
| @Column(name = "request_status", nullable = false) | ||
|
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. A
|
||
| private ShopToOrderableRequestStatus requestStatus = ShopToOrderableRequestStatus.PENDING; | ||
|
|
||
| @Column(name = "approved_at", columnDefinition = "TIMESTAMP") | ||
| private LocalDateTime approvedAt = null; | ||
|
|
||
| @Builder | ||
| public ShopToOrderable( | ||
| Shop shop, | ||
| Integer minimumOrderAmount, | ||
| Boolean isTakeout, | ||
| ShopToOrderableDeliveryOption deliveryOption, | ||
| Integer campusDeliveryTip, | ||
| Integer offCampusDeliveryTip, | ||
| String businessLicenseUrl, | ||
| String businessCertificateUrl, | ||
| String bankCopyUrl, | ||
| String bank, | ||
| String accountNumber | ||
| ) { | ||
| this.shop = shop; | ||
| this.minimumOrderAmount = minimumOrderAmount; | ||
| this.isTakeout = isTakeout; | ||
| this.deliveryOption = deliveryOption; | ||
| this.campusDeliveryTip = campusDeliveryTip; | ||
| this.offCampusDeliveryTip = offCampusDeliveryTip; | ||
| this.businessLicenseUrl = businessLicenseUrl; | ||
| this.businessCertificateUrl = businessCertificateUrl; | ||
| this.bankCopyUrl = bankCopyUrl; | ||
| this.bank = bank; | ||
| this.accountNumber = accountNumber; | ||
| this.approvedAt = null; | ||
| } | ||
|
|
||
| public void approveRequest() { | ||
| this.requestStatus = ShopToOrderableRequestStatus.APPROVED; | ||
| this.approvedAt = LocalDateTime.now(); | ||
| } | ||
|
|
||
| public void rejectRequest() { | ||
| this.requestStatus = ShopToOrderableRequestStatus.REJECTED; | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,7 @@ | ||
| package in.koreatech.koin.domain.shoptoOrderable.model; | ||
|
|
||
| public enum ShopToOrderableDeliveryOption { | ||
| CAMPUS, | ||
| OUTSIDE, | ||
| BOTH | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,5 @@ | ||
| package in.koreatech.koin.domain.shoptoOrderable.model; | ||
|
|
||
| public enum ShopToOrderableRequestStatus { | ||
| PENDING, APPROVED, REJECTED | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,15 @@ | ||
| package in.koreatech.koin.domain.shoptoOrderable.repository; | ||
|
|
||
| import in.koreatech.koin.domain.shoptoOrderable.model.ShopToOrderable; | ||
| import in.koreatech.koin.domain.shoptoOrderable.model.ShopToOrderableRequestStatus; | ||
|
|
||
| import org.springframework.data.repository.Repository; | ||
|
|
||
asa9874 marked this conversation as resolved.
Show resolved
Hide resolved
|
||
| public interface ShopToOrderableRepository extends Repository<ShopToOrderable, Integer> { | ||
|
|
||
| ShopToOrderable save(ShopToOrderable shopToOrderable); | ||
|
|
||
| boolean existsByShopId(Integer shopId); | ||
|
|
||
| boolean existsByShopIdAndRequestStatus(Integer shopId, ShopToOrderableRequestStatus requestStatus); | ||
| } | ||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,66 @@ | ||
| package in.koreatech.koin.domain.shoptoOrderable.service; | ||
|
|
||
| import org.springframework.stereotype.Service; | ||
| import org.springframework.transaction.annotation.Transactional; | ||
|
|
||
| import in.koreatech.koin.domain.shop.model.shop.Shop; | ||
| import in.koreatech.koin.domain.shop.repository.shop.ShopRepository; | ||
| import in.koreatech.koin.domain.shoptoOrderable.dto.ShopToOrderableRequest; | ||
| import in.koreatech.koin.domain.shoptoOrderable.model.ShopToOrderable; | ||
| import in.koreatech.koin.domain.shoptoOrderable.repository.ShopToOrderableRepository; | ||
| import in.koreatech.koin.global.exception.CustomException; | ||
| import lombok.RequiredArgsConstructor; | ||
|
|
||
| import static in.koreatech.koin.domain.shoptoOrderable.model.ShopToOrderableRequestStatus.*; | ||
| import static in.koreatech.koin.global.code.ApiResponseCode.*; | ||
|
|
||
| @Service | ||
| @RequiredArgsConstructor | ||
| public class ShopToOrderableService { | ||
|
|
||
| private final ShopToOrderableRepository shopToOrderableRepository; | ||
| private final ShopRepository shopRepository; | ||
|
|
||
| @Transactional | ||
| public void createOrderableRequest(Integer ownerId, ShopToOrderableRequest request, Integer shopId) { | ||
| Shop shop = shopRepository.getById(shopId); | ||
|
|
||
| validateShopOwner(shop, ownerId); | ||
| validateDuplicateRequest(shop); | ||
|
|
||
| ShopToOrderable shopToOrderable = ShopToOrderable.builder() | ||
| .shop(shop) | ||
| .minimumOrderAmount(request.minimumOrderAmount()) | ||
| .isTakeout(request.isTakeout()) | ||
| .deliveryOption(request.deliveryOption()) | ||
| .campusDeliveryTip(request.campusDeliveryTip()) | ||
| .offCampusDeliveryTip(request.offCampusDeliveryTip()) | ||
| .businessLicenseUrl(request.businessLicenseUrl()) | ||
| .businessCertificateUrl(request.businessCertificateUrl()) | ||
| .bankCopyUrl(request.bankCopyUrl()) | ||
| .bank(request.bank()) | ||
| .accountNumber(request.accountNumber()) | ||
| .build(); | ||
|
|
||
| shopToOrderableRepository.save(shopToOrderable); | ||
| } | ||
|
|
||
| private void validateShopOwner(Shop shop, Integer ownerId) { | ||
| // 가게 사장님인지 확인 | ||
| if (!shop.isOwner(ownerId)) { | ||
| throw CustomException.of(FORBIDDEN_SHOP_OWNER, "ownerId: " + ownerId + ", shopId: " + shop.getId()); | ||
| } | ||
| } | ||
|
|
||
| private void validateDuplicateRequest(Shop shop) { | ||
| // 이미 신청한 내역이 있는지 확인 | ||
| if (shopToOrderableRepository.existsByShopIdAndRequestStatus(shop.getId(), PENDING)) { | ||
| throw CustomException.of(DUPLICATE_REQUESTED_ORDERABLE_SHOP, "shopId: " + shop.getId()); | ||
| } | ||
|
|
||
| // 이미 주문가능 상점인지 확인 | ||
| if (shopToOrderableRepository.existsByShopIdAndRequestStatus(shop.getId(), APPROVED)) { | ||
| throw CustomException.of(DUPLICATE_ORDERABLE_SHOP, "shopId: " + shop.getId()); | ||
| } | ||
| } | ||
| } |
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.
C
ShopToOrderable으로 확정하신건가용개인적으로는
ownershop패키지 하위로 들어가도 괜찮을 거 같은데 어떻게 생각하시나요 ??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.
해당 스프린트가 아직 명확하게 요구사항이 정해지지 않아서, 우선 독립적으로 패키지를 만들어 제작을 시작했습니다.
추후 요구사항이 명확해지게 되면, 프로젝트 구조에 맞게 패키지 구조를 조정할려고 하는데 괜찮을까요?
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.
개인적인 생각으로는 요구사항이랑 별개로
주변상점 -> 주문가능상점은 무조건 필요해서 input/output이 변하는 거 말고는 크게 변경될 사항이 없을 거 같아요. (사장님은주변상점 -> 주문가능상점신청, 이력 조회, 취소 / 어드민은 승인, 반려) 사장님이 상점에 대해 조작을 가하는 거기 때문에ownershop패키지 하위에 들어가도 괜찮지 않을까 생각을 했습니다.개인적으로
ShopToOrderable네이밍을 조금만 더 다듬어보면 좋을 거 같아요.주변 상점 -> 주문 서비스(주문 가능 상점)를 신청한 이력을 관리하고 있으니ShopOrderServiceRequest도 괜찮아 보입니다.종범님의 생각이 궁금합니다 👀
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.
네
ownershop패키지의 하위로 들어가는게 좋을거같습니다.네이밍은
ShopApprovalRequest|ShopEnableRequest로 생각하고 있었는데, 의미적으로 뭔가 명확하게 다가오지 않아서ShopOrderServiceRequest가 좋을거같습니다.ownershop패키지의 구성이 그러면 다음처럼 변경하면 될까요?=>