Skip to content
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,14 @@
public interface ProductImgRepository extends JpaRepository<ProductImg, Long> {
List<ProductImg> findAllByIdInOrderByIdAsc(List<Long> imageIds);

@Query("""
SELECT pi FROM ProductImg pi
WHERE pi.board.id IN :boardIds
AND pi.imgOrder = 0
AND pi.isDeleted = false
""")
List<ProductImg> findThumbnailsByBoardIdIn(@Param("boardIds") List<Long> boardIds);

@Modifying(clearAutomatically = true)
@Query("""
UPDATE ProductImg pi
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -41,4 +41,13 @@ public interface ProductRepository extends JpaRepository<Product, Long>, Product
WHERE p.id IN :productIds
""")
void softDeleteByProductIds(@Param("productIds") List<Long> productIds);

@Query("""
SELECT p FROM Product p
JOIN FETCH p.board b
JOIN FETCH b.store s
WHERE p.id IN :ids
AND p.isDeleted = false
""")
List<Product> findAllWithBoardAndStoreByIdIn(@Param("ids") List<Long> ids);
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,10 @@ public class CustomerApiPath {
public static final String PREFIX = "/api/v1/customer";

public static final String[] ANY_METHOD = {
"/api/v1/boards/folders/**"
"/api/v1/boards/folders/**",
"/api/v1/customer/orders/**",
"/api/v1/customer/delivery-addresses/**",
"/api/v1/customer/payments/**"
};

}
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,12 @@ public static Shipping empty() {
return new Shipping(null, null, null);
}

public static Shipping withMemo(String deliveryMemo) {
Shipping shipping = new Shipping(null, null, null);
shipping.deliveryMemo = deliveryMemo;
return shipping;
}

public void updateShippingInfo(String courierName, String trackingNumber) {
this.courierName = courierName;
this.trackingNumber = trackingNumber;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -207,7 +207,18 @@ public enum BbangleErrorCode {
INVALID_VAT_DATE_RANGE(-852, "조회 기간이 올바르지 않습니다.", BAD_REQUEST),
EXCEEDED_MAX_VAT_DATE_RANGE(-853, "조회 기간은 최대 1년까지 가능합니다.", BAD_REQUEST),
INVALID_EXCEL_TYPE(-854, "엑셀 다운로드 유형이 올바르지 않습니다.", BAD_REQUEST),
EXCEL_CREATE_FAILED(-855, "엑셀 파일 생성에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR);
EXCEL_CREATE_FAILED(-855, "엑셀 파일 생성에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),

// Order Customer Error(856 ~ 870)
DELIVERY_ADDRESS_NOT_FOUND(-856, "기본 배송지를 찾을 수 없습니다.", NOT_FOUND),
ORDER_PRODUCT_EMPTY(-857, "주문 상품이 비어 있습니다.", BAD_REQUEST),

// Payment Customer Error(871 ~ 880)
PAYMENT_NOT_FOUND(-871, "결제 정보를 찾을 수 없습니다.", NOT_FOUND),
PAYMENT_AMOUNT_MISMATCH(-872, "결제 금액이 일치하지 않습니다.", BAD_REQUEST),
PAYMENT_CONFIRM_FAILED(-873, "결제 승인에 실패했습니다.", HttpStatus.INTERNAL_SERVER_ERROR),
PAYMENT_ALREADY_COMPLETED(-874, "이미 완료된 결제입니다.", CONFLICT),
INVALID_ORDER_STATUS_TRANSITION(-875, "해당 주문 상태에서는 상태를 변경할 수 없습니다.", BAD_REQUEST);

private final int code;
private final String message;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
package com.bbangle.bbangle.member.customer.controller;

import com.bbangle.bbangle.common.dto.CommonResult;
import com.bbangle.bbangle.common.dto.ListResult;
import com.bbangle.bbangle.common.dto.SingleResult;
import com.bbangle.bbangle.common.service.ResponseService;
import com.bbangle.bbangle.member.customer.controller.dto.request.DeliveryAddressSaveRequest;
import com.bbangle.bbangle.member.customer.controller.dto.response.DeliveryAddressResponse;
import com.bbangle.bbangle.member.customer.service.DeliveryAddressService;
import io.swagger.v3.oas.annotations.Operation;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
@RequestMapping("/api/v1/customer/delivery-addresses")
public class DeliveryAddressController {

private final DeliveryAddressService deliveryAddressService;
private final ResponseService responseService;

@GetMapping
@Operation(summary = "배송지 목록 조회")
public ListResult<DeliveryAddressResponse> getDeliveryAddresses(
@AuthenticationPrincipal Long memberId
) {
return responseService.getListResult(
deliveryAddressService.getDeliveryAddresses(memberId)
);
}

@PostMapping
@Operation(summary = "배송지 추가")
public SingleResult<DeliveryAddressResponse> addDeliveryAddress(
@Valid @RequestBody DeliveryAddressSaveRequest request,
@AuthenticationPrincipal Long memberId
) {
return responseService.getSingleResult(
deliveryAddressService.addDeliveryAddress(memberId, request)
);
}

@PutMapping("/{id}")
@Operation(summary = "배송지 수정")
public SingleResult<DeliveryAddressResponse> updateDeliveryAddress(
@PathVariable Long id,
@Valid @RequestBody DeliveryAddressSaveRequest request,
@AuthenticationPrincipal Long memberId
) {
return responseService.getSingleResult(
deliveryAddressService.updateDeliveryAddress(memberId, id, request)
);
}

@DeleteMapping("/{id}")
@Operation(summary = "배송지 삭제")
public CommonResult deleteDeliveryAddress(
@PathVariable Long id,
@AuthenticationPrincipal Long memberId
) {
deliveryAddressService.deleteDeliveryAddress(memberId, id);
return responseService.getSuccessResult();
}

@PatchMapping("/{id}/default")
@Operation(summary = "기본 배송지 설정")
public CommonResult setDefaultDeliveryAddress(
@PathVariable Long id,
@AuthenticationPrincipal Long memberId
) {
deliveryAddressService.setDefaultDeliveryAddress(memberId, id);
return responseService.getSuccessResult();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package com.bbangle.bbangle.member.customer.controller.dto.request;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.Size;

public record DeliveryAddressSaveRequest(
@NotBlank @Size(max = 50) String addressName,
@NotBlank @Size(max = 100) String recipientName,
@NotBlank @Size(max = 20) String phone,
@NotBlank @Size(max = 255) String address,
@Size(max = 255) String addressDetail,
@Size(max = 10) String zipCode,
Boolean isDefault
) {
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
package com.bbangle.bbangle.member.customer.controller.dto.response;

import com.bbangle.bbangle.member.domain.MemberDeliveryAddress;

public record DeliveryAddressResponse(
Long id,
String addressName,
boolean isDefault,
String recipientName,
String phone,
String address,
String addressDetail,
String zipCode
) {
public static DeliveryAddressResponse from(MemberDeliveryAddress entity) {
return new DeliveryAddressResponse(
entity.getId(),
entity.getAddressName(),
entity.isDefault(),
entity.getRecipientName(),
entity.getPhone(),
entity.getAddress(),
entity.getAddressDetail(),
entity.getZipCode()
);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
package com.bbangle.bbangle.member.customer.service;

import com.bbangle.bbangle.exception.BbangleErrorCode;
import com.bbangle.bbangle.exception.BbangleException;
import com.bbangle.bbangle.member.customer.controller.dto.request.DeliveryAddressSaveRequest;
import com.bbangle.bbangle.member.customer.controller.dto.response.DeliveryAddressResponse;
import com.bbangle.bbangle.member.domain.Member;
import com.bbangle.bbangle.member.domain.MemberDeliveryAddress;
import com.bbangle.bbangle.member.repository.MemberDeliveryAddressRepository;
import com.bbangle.bbangle.member.repository.MemberRepository;
import java.util.List;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
@RequiredArgsConstructor
@Transactional
public class DeliveryAddressService {

private final MemberDeliveryAddressRepository deliveryAddressRepository;
private final MemberRepository memberRepository;

@Transactional(readOnly = true)
public List<DeliveryAddressResponse> getDeliveryAddresses(Long memberId) {
return deliveryAddressRepository.findAllByMemberIdAndIsDeletedFalse(memberId)
.stream()
.map(DeliveryAddressResponse::from)
.toList();
}

public DeliveryAddressResponse addDeliveryAddress(Long memberId, DeliveryAddressSaveRequest request) {
boolean makeDefault = Boolean.TRUE.equals(request.isDefault());

// isDefault=true일 때 Member 행 잠금을 직렬화 포인트로 사용해 동시 insert 경합 방지
Member member = makeDefault
? memberRepository.findByIdWithLock(memberId)
.orElseThrow(() -> new BbangleException(BbangleErrorCode.NOTFOUND_MEMBER))
: memberRepository.findById(memberId)
.orElseThrow(() -> new BbangleException(BbangleErrorCode.NOTFOUND_MEMBER));

if (makeDefault) {
deliveryAddressRepository.findByMemberIdAndIsDefaultTrueAndIsDeletedFalse(memberId)
.ifPresent(MemberDeliveryAddress::unsetDefault);
}

MemberDeliveryAddress address = MemberDeliveryAddress.builder()
.member(member)
.addressName(request.addressName())
.recipientName(request.recipientName())
.phone(request.phone())
.address(request.address())
.addressDetail(request.addressDetail())
.zipCode(request.zipCode())
.isDefault(makeDefault)
.build();

return DeliveryAddressResponse.from(deliveryAddressRepository.save(address));
}

public DeliveryAddressResponse updateDeliveryAddress(Long memberId, Long addressId,
DeliveryAddressSaveRequest request) {
MemberDeliveryAddress address = findOwnedAddress(memberId, addressId);

address.update(
request.addressName(),
request.recipientName(),
request.phone(),
request.address(),
request.addressDetail(),
request.zipCode()
);

// isDefault가 null이면 기존 기본 배송지 상태 유지
if (request.isDefault() != null) {
boolean makeDefault = request.isDefault();
if (makeDefault && !address.isDefault()) {
// Member 행 잠금으로 직렬화 후 기존 기본 배송지 해제
memberRepository.findByIdWithLock(memberId)
.orElseThrow(() -> new BbangleException(BbangleErrorCode.NOTFOUND_MEMBER));
deliveryAddressRepository.findByMemberIdAndIsDefaultTrueAndIsDeletedFalse(memberId)
.ifPresent(MemberDeliveryAddress::unsetDefault);
address.setDefault();
} else if (!makeDefault) {
Comment thread
CUCU7103 marked this conversation as resolved.
address.unsetDefault();
}
}

return DeliveryAddressResponse.from(address);
}

public void deleteDeliveryAddress(Long memberId, Long addressId) {
MemberDeliveryAddress address = findOwnedAddress(memberId, addressId);
address.delete();
}

public void setDefaultDeliveryAddress(Long memberId, Long addressId) {
// Member 행 잠금을 먼저 획득해 동시 기본 배송지 변경 직렬화
memberRepository.findByIdWithLock(memberId)
.orElseThrow(() -> new BbangleException(BbangleErrorCode.NOTFOUND_MEMBER));

MemberDeliveryAddress address = findOwnedAddress(memberId, addressId);
deliveryAddressRepository.findByMemberIdAndIsDefaultTrueAndIsDeletedFalse(memberId)
.ifPresent(MemberDeliveryAddress::unsetDefault);
address.setDefault();
}

// 소유권을 조회 조건에 포함해 타인 주소 접근 시에도 동일한 404 반환 (ID 열거 방지)
private MemberDeliveryAddress findOwnedAddress(Long memberId, Long addressId) {
return deliveryAddressRepository
.findByIdAndMemberIdAndIsDeletedFalse(addressId, memberId)
.orElseThrow(() -> new BbangleException(BbangleErrorCode.DELIVERY_ADDRESS_NOT_FOUND));
}
}
Loading