From fad4e32e0e32d53dae1172ec6bb9f903373735a0 Mon Sep 17 00:00:00 2001 From: jihoon Date: Wed, 11 Mar 2026 14:12:54 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20Store=20=EC=A3=BC=EB=AC=B8=20=EA=B0=80?= =?UTF-8?q?=EB=8A=A5=20=EC=A3=BC=EC=97=AD=20=EC=A2=85=EB=A1=9C=EA=B5=AC?= =?UTF-8?q?=EB=A1=9C=20=EC=A0=9C=ED=95=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../store/controller/StoreController.java | 4 +- .../domain/store/service/StoreService.java | 26 ++- .../global/exception/ErrorCode.java | 1 + .../domain/store/StoreControllerTest.java | 60 +++++++ .../domain/store/StoreServiceTest.java | 148 +++++++++++++++++- 5 files changed, 234 insertions(+), 5 deletions(-) diff --git a/src/main/java/com/babjo/deliverycommerce/domain/store/controller/StoreController.java b/src/main/java/com/babjo/deliverycommerce/domain/store/controller/StoreController.java index 41fcefb..72e771a 100644 --- a/src/main/java/com/babjo/deliverycommerce/domain/store/controller/StoreController.java +++ b/src/main/java/com/babjo/deliverycommerce/domain/store/controller/StoreController.java @@ -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 아님)") }) @@ -116,7 +116,7 @@ public ResponseEntity> 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 = "존재하지 않는 가게") diff --git a/src/main/java/com/babjo/deliverycommerce/domain/store/service/StoreService.java b/src/main/java/com/babjo/deliverycommerce/domain/store/service/StoreService.java index e4952f6..889337e 100644 --- a/src/main/java/com/babjo/deliverycommerce/domain/store/service/StoreService.java +++ b/src/main/java/com/babjo/deliverycommerce/domain/store/service/StoreService.java @@ -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; @@ -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를 기반으로 새로운 가게를 생성 @@ -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(), @@ -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); } @@ -185,4 +203,10 @@ public List 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); + } + } } \ No newline at end of file diff --git a/src/main/java/com/babjo/deliverycommerce/global/exception/ErrorCode.java b/src/main/java/com/babjo/deliverycommerce/global/exception/ErrorCode.java index 6ef25ea..19cea36 100644 --- a/src/main/java/com/babjo/deliverycommerce/global/exception/ErrorCode.java +++ b/src/main/java/com/babjo/deliverycommerce/global/exception/ErrorCode.java @@ -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 ───────────────────────────────────────── diff --git a/src/test/java/com/babjo/deliverycommerce/domain/store/StoreControllerTest.java b/src/test/java/com/babjo/deliverycommerce/domain/store/StoreControllerTest.java index 0c210b7..5acc26e 100644 --- a/src/test/java/com/babjo/deliverycommerce/domain/store/StoreControllerTest.java +++ b/src/test/java/com/babjo/deliverycommerce/domain/store/StoreControllerTest.java @@ -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; @@ -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")); + } } \ No newline at end of file diff --git a/src/test/java/com/babjo/deliverycommerce/domain/store/StoreServiceTest.java b/src/test/java/com/babjo/deliverycommerce/domain/store/StoreServiceTest.java index 45b1c55..29b9dad 100644 --- a/src/test/java/com/babjo/deliverycommerce/domain/store/StoreServiceTest.java +++ b/src/test/java/com/babjo/deliverycommerce/domain/store/StoreServiceTest.java @@ -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; @@ -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; @@ -27,6 +32,7 @@ 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 { @@ -34,9 +40,127 @@ 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() { @@ -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, @@ -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 @@ -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); + } + } } \ No newline at end of file