Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ public StoreController(StoreService storeService, CurrentUserResolver currentUse
)
@ApiResponses({
@ApiResponse(responseCode = "201", description = "생성 성공"),
@ApiResponse(responseCode = "400", description = "요청 값 검증 실패"),
@ApiResponse(responseCode = "400", description = "요청 값 검증 실패 / 주문 가능 구역 정책 위반"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "403", description = "권한 없음(OWNER 아님)")
})
Expand Down Expand Up @@ -116,7 +116,7 @@ public ResponseEntity<CommonResponse<StoreResponseDto>> get(@PathVariable UUID s
)
@ApiResponses({
@ApiResponse(responseCode = "200", description = "수정 성공"),
@ApiResponse(responseCode = "400", description = "요청 값 검증 실패"),
@ApiResponse(responseCode = "400", description = "요청 값 검증 실패 / 주문 가능 구역 정책 위반"),
@ApiResponse(responseCode = "401", description = "인증 실패"),
@ApiResponse(responseCode = "403", description = "권한 없음(OWNER 아님)"),
@ApiResponse(responseCode = "404", description = "존재하지 않는 가게")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.babjo.deliverycommerce.domain.store.dto.StoreUpdateRequestDto;
import com.babjo.deliverycommerce.domain.store.entity.Store;
import com.babjo.deliverycommerce.domain.store.repository.StoreRepository;
import com.babjo.deliverycommerce.domain.user.entity.User;
import com.babjo.deliverycommerce.domain.user.repository.UserRepository;
import com.babjo.deliverycommerce.global.exception.CustomException;
import com.babjo.deliverycommerce.global.exception.ErrorCode;
import lombok.extern.slf4j.Slf4j;
Expand All @@ -25,11 +27,19 @@ public class StoreService {
private static final int PAGE_SIZE = 10;

private final StoreRepository storeRepository;
private final UserRepository userRepository;

public StoreService(StoreRepository storeRepository) {
public StoreService(StoreRepository storeRepository, UserRepository userRepository) {
this.storeRepository = storeRepository;
this.userRepository = userRepository;
}

/*
* 주문 가능 구역 정책
* 종로구 주소만 주문 가능
*/
private static final String DELIVERY_AVAILABLE_GU = "종로구";

/**
* 가게 생성
* - ownerId를 기반으로 새로운 가게를 생성
Expand All @@ -38,6 +48,12 @@ public StoreService(StoreRepository storeRepository) {
public StoreResponseDto create(Long ownerId, StoreCreateRequestDto request) {
log.info("가게 생성 요청 - ownerId={}, category={}, name={}", ownerId, request.getCategory(), request.getName());

// owner 확인
User owner = userRepository.findById(ownerId).orElseThrow(() -> new CustomException(ErrorCode.USER_NOT_FOUND));

//주문 가능 구역 검증(종로구만)
validateDeliveryArea(request.getAddress());

Store store = Store.create(
ownerId,
request.getCategory(),
Expand Down Expand Up @@ -80,6 +96,8 @@ public StoreResponseDto update(UUID storeId, Long actorUserId, StoreUpdateReques
request.getAddress()
);

validateDeliveryArea(store.getAddress());

log.info("가게 수정 완료 - storeId={}, actorUserId={}, updatedName={}", store.getStoreId(), actorUserId, store.getName());
return StoreResponseDto.from(store);
}
Expand Down Expand Up @@ -185,4 +203,10 @@ public List<StoreListResponseDto> getStores(String category, String name, int pa
.map(store -> StoreListResponseDto.from(store))
.toList();
}

private void validateDeliveryArea(String address) {
if (address == null || address.isBlank() || !address.contains(DELIVERY_AVAILABLE_GU)) {
throw new CustomException(ErrorCode.STORE_ADDRESS_NOT_SUPPORTED);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -63,6 +63,7 @@ public enum ErrorCode {
// ── Store ─────────────────────────────────────────
STORE_NOT_FOUND(HttpStatus.NOT_FOUND, "STORE_NOT_FOUND", "존재하지 않는 가게입니다."),
STORE_FORBIDDEN(HttpStatus.FORBIDDEN, "STORE_FORBIDDEN", "해당 가게에 대한 권한이 없습니다."),
STORE_ADDRESS_NOT_SUPPORTED(HttpStatus.BAD_REQUEST, "STORE_ADDRESS_NOT_SUPPORTED", "현재 주문 가능 지역은 종로구만 지원합니다."),


// ── Cart ─────────────────────────────────────────
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@
import com.babjo.deliverycommerce.domain.store.dto.StoreResponseDto;
import com.babjo.deliverycommerce.domain.store.dto.StoreUpdateRequestDto;
import com.babjo.deliverycommerce.domain.store.service.StoreService;
import com.babjo.deliverycommerce.global.exception.CustomException;
import com.babjo.deliverycommerce.global.exception.ErrorCode;
import com.babjo.deliverycommerce.global.security.CurrentUserResolver;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
Expand Down Expand Up @@ -295,4 +297,62 @@ void deleteStore_success() throws Exception {

verifyNoInteractions(currentUserResolver, storeService);
}

@Test
@WithMockUser(roles = "OWNER")
@DisplayName("가게생성 - 종로구가 아니면 400")
void 종로구_생성아니면_400() throws Exception {
//given
given(currentUserResolver.getUserId(any(Authentication.class))).willReturn(1L);

given(storeService.create(eq(1L), any(StoreCreateRequestDto.class)))
.willThrow(new CustomException(ErrorCode.STORE_ADDRESS_NOT_SUPPORTED));


String requestBody = """
{
"category": "한식",
"name": "한식당",
"address": "서울시 강남구"
}
""";

//when & then
mockMvc.perform(post("/v1/stores")
.with(csrf())
.contentType("application/json")
.content(requestBody))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.status").value(400))
.andExpect(jsonPath("$.code").value("STORE_ADDRESS_NOT_SUPPORTED"));
}

@Test
@WithMockUser(roles = "OWNER")
@DisplayName("가게 수정 시 종로구가 아닌 주소로 변경하면 400")
void 다른주소_400() throws Exception {
//given
UUID storeId = UUID.randomUUID();

given(currentUserResolver.getUserId(any(Authentication.class))).willReturn(1L);

given(storeService.update(eq(storeId), eq(1L), any(StoreUpdateRequestDto.class))).willThrow(new CustomException(ErrorCode.STORE_ADDRESS_NOT_SUPPORTED));

String requestBody = """
{
"category": "치킨",
"name": "치킨집",
"address": "서울시 강남구"
}
""";

//when & then
mockMvc.perform(patch("/v1/stores/{storeId}", storeId)
.with(csrf())
.contentType("application/json")
.content(requestBody))
.andExpect(status().isBadRequest())
.andExpect(jsonPath("$.status").value(400))
.andExpect(jsonPath("$.code").value("STORE_ADDRESS_NOT_SUPPORTED"));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,13 @@
import com.babjo.deliverycommerce.domain.store.entity.Store;
import com.babjo.deliverycommerce.domain.store.repository.StoreRepository;
import com.babjo.deliverycommerce.domain.store.service.StoreService;
import com.babjo.deliverycommerce.domain.user.entity.User;
import com.babjo.deliverycommerce.domain.user.repository.UserRepository;
import com.babjo.deliverycommerce.global.common.enums.UserEnumRole;
import com.babjo.deliverycommerce.global.exception.CustomException;
import com.babjo.deliverycommerce.global.exception.ErrorCode;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
Expand All @@ -19,6 +23,7 @@
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;

import java.lang.reflect.Field;
import java.util.List;
import java.util.Optional;
import java.util.UUID;
Expand All @@ -27,16 +32,135 @@
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.BDDMockito.given;
import static org.mockito.BDDMockito.then;

@ExtendWith(MockitoExtension.class)
class StoreServiceTest {

@Mock
private StoreRepository storeRepository;

@Mock
UserRepository userRepository;

@InjectMocks
private StoreService storeService;

@Nested
@DisplayName("가게 생성 - 종로구 주문 가능 정책")
class Create {

@Test
@DisplayName("주소가 종로구이면 생성 성공")
void 종로구_생성() throws Exception {
//given
Long ownerId = 1L;

User owner = User.createForTest(ownerId, "owner1", "owner1@test.com", "사장님", UserEnumRole.OWNER);

given(userRepository.findById(ownerId)).willReturn(Optional.of(owner));

StoreCreateRequestDto request = new StoreCreateRequestDto();
setField(request, "category", "한식");
setField(request, "name", "한식당");
setField(request, "address", "서울특별시 종로구 청계천로 1");

Store saved = Store.create(ownerId, "한식", "한식당", "서울특별시 종로구 청계천로 1");
given(storeRepository.save(any(Store.class))).willReturn(saved);

//when
StoreResponseDto result = storeService.create(ownerId, request);

//then
assertThat(result).isNotNull();
assertThat(result.getAddress()).contains("종로구");
then(storeRepository).should().save(any(Store.class));

}

@Test
@DisplayName("주소가 종로구가 아니면 STORE_ADDRESS_NOT_SUPPORTED")
void 종로구_아닐시_실패() throws Exception {
//given
Long ownerId = 1L;

User owner = User.createForTest(ownerId, "owner1", "owner1@test.com", "사장님", UserEnumRole.OWNER);
given(userRepository.findById(ownerId)).willReturn(Optional.of(owner));

StoreCreateRequestDto request = new StoreCreateRequestDto();
setField(request, "category", "한식");
setField(request, "name", "한식당");
setField(request, "address", "서울특별시 강남구 테헤란로 1");

//when & then
assertThatThrownBy(() -> storeService.create(ownerId, request))
.isInstanceOf(CustomException.class)
.satisfies(ex -> {
CustomException ce = (CustomException) ex;
assertThat(ce.getErrorCode()).isEqualTo(ErrorCode.STORE_ADDRESS_NOT_SUPPORTED);
});

then(storeRepository).shouldHaveNoInteractions();
}
}

@Nested
@DisplayName("가게 수정 종로구 주문 가능 정책")
class Update {

@Test
@DisplayName("수정 후 주소가 종로구이면 성공")
void 수정주소_종로() throws Exception {
//given
Long ownerId = 1L;
UUID storeId = UUID.randomUUID();

Store store = Store.create(ownerId, "한식", "기존가게", "서울특별시 강남구 테헤란로 1");
setField(store, "storeId", storeId);

given(storeRepository.findByStoreIdAndDeletedAtIsNull(storeId)).willReturn(Optional.of(store));

StoreUpdateRequestDto request = new StoreUpdateRequestDto();
setField(request, "category", "한식");
setField(request, "name", "한식당");
setField(request, "address", "서울특별시 종로구 새주소 1");

//when
StoreResponseDto result = storeService.update(storeId, ownerId, request);

//then
assertThat(result).isNotNull();
assertThat(result.getAddress()).contains("종로구");
}

@Test
@DisplayName("수정 후 주소가 종로구가 아니면 STORE_ADDRESS_NOT_SUPPORTED")
void 수정후_종로구_아닐시_실패() throws Exception {
//givne
Long ownerId = 1L;
UUID storeId = UUID.randomUUID();

Store store = Store.create(ownerId, "한식", "기존가게", "서울특별시 종로구 기존주소 1");
setField(store, "storeId", storeId);

given(storeRepository.findByStoreIdAndDeletedAtIsNull(storeId)).willReturn(Optional.of(store));

StoreUpdateRequestDto request = new StoreUpdateRequestDto();
setField(request, "category", "한식");
setField(request, "name", "한식당");
setField(request, "address", "서울특별시 강남구 테해란로 1");

//when & then
assertThatThrownBy(() -> storeService.update(storeId, ownerId, request))
.isInstanceOf(CustomException.class)
.satisfies(ex -> {
CustomException ce = (CustomException) ex;
assertThat(ce.getErrorCode()).isEqualTo(ErrorCode.STORE_ADDRESS_NOT_SUPPORTED);
});
}
}


@Test
@DisplayName("존재하지 않는 가게를 조회하면 STORE_NOT_FOUND 예외가 발생한다")
void getStore_fail_whenStoreNotFound() {
Expand Down Expand Up @@ -115,11 +239,21 @@ void create_success() {
//given
Long ownerId = 1L;

User owner = User.createForTest(
ownerId,
"owner1",
"owner1@test.com",
"사장님",
UserEnumRole.OWNER
);

StoreCreateRequestDto request = new StoreCreateRequestDto(
"KOREAN",
"밥집",
"서울시 강남구"
"서울시 종로구"
);
given(userRepository.findById(ownerId))
.willReturn(Optional.of(owner));

Store saveStore = Store.create(
ownerId,
Expand All @@ -137,7 +271,7 @@ void create_success() {
//then
assertThat(response.getCategory()).isEqualTo("KOREAN");
assertThat(response.getName()).isEqualTo("밥집");
assertThat(response.getAddress()).isEqualTo("서울시 강남구");
assertThat(response.getAddress()).isEqualTo("서울시 종로구");
}

@Test
Expand Down Expand Up @@ -261,4 +395,14 @@ void getStores_success_whenNameCondition() {
assertThat(response.get(0).getName()).contains("치킨");
assertThat(response.get(1).getName()).contains("치킨");
}

private static void setField(Object target, String fieldName, Object value) throws Exception {
try {
var f = target.getClass().getDeclaredField(fieldName);
f.setAccessible(true);
f.set(target, value);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new RuntimeException("setField 실패: " + target.getClass().getSimpleName() + "." + fieldName, e);
}
}
}
Loading