Repository: 2NE1-TEAM/secondCoupang
Pull request #30: 스토어 주문, 삭제, 단건조회 로직 구현 및 전화번호 검증 utils 구현
Author: login: jnjongjeon
Labels: name: ✏ feat
Created: 2025-03-15T10:27:25Z | Merged: 2025-03-16T00:35:10Z

## Description
1. 스토어 생성, 삭제, 단건 조회 구현
2. 전화번호 통일성 위한 phonNumberUtils 구현
3. 1,2번 따른 테스트 코드 작성

Resolves: #26 

## PR Type

- [x] 새로운 기능 추가
- [ ] 버그 수정
- [ ] 코드에 영향을 주지 않는 변경사항(오타 수정, 탭 사이즈 변경, 변수명 변경)
- [ ] 코드 리팩토링
- [ ] 주석 추가 및 수정
- [ ] 문서 수정
- [x] 테스트 추가, 테스트 리팩토링
- [ ] 빌드 부분 혹은 패키지 매니저 수정
- [ ] 파일 혹은 폴더명 수정
- [ ] 파일 혹은 폴더 삭제

## PR Checklist

- [x] 커밋 메시지 컨벤션에 맞게 작성했습니다.
- [x] 변경 사항에 대한 테스트를 했습니다.(버그 수정/기능에 대한 테스트).



Diff:
diff --git a/store/build.gradle b/store/build.gradle
index 805c924..fe27090 100644
--- a/store/build.gradle
+++ b/store/build.gradle
@@ -31,6 +31,8 @@ dependencies {
 	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
 	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
 	implementation 'org.springframework.boot:spring-boot-starter-web'
+	implementation 'org.springframework.boot:spring-boot-starter-validation'
+	implementation 'org.springframework.cloud:spring-cloud-starter-openfeign'
 	compileOnly 'org.projectlombok:lombok'
 	runtimeOnly 'com.mysql:mysql-connector-j'
 	annotationProcessor 'org.projectlombok:lombok'
diff --git a/store/src/main/java/com/toanyone/store/StoreApplication.java b/store/src/main/java/com/toanyone/store/StoreApplication.java
index 3ef2d2a..0607c1a 100644
--- a/store/src/main/java/com/toanyone/store/StoreApplication.java
+++ b/store/src/main/java/com/toanyone/store/StoreApplication.java
@@ -2,9 +2,11 @@
 
 import org.springframework.boot.SpringApplication;
 import org.springframework.boot.autoconfigure.SpringBootApplication;
+import org.springframework.cloud.openfeign.EnableFeignClients;
 import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
 
 @EnableJpaAuditing
+@EnableFeignClients
 @SpringBootApplication
 public class StoreApplication {
 
diff --git a/store/src/main/java/com/toanyone/store/application/service/StoreServiceImpl.java b/store/src/main/java/com/toanyone/store/application/service/StoreServiceImpl.java
new file mode 100644
index 0000000..68efbd0
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/application/service/StoreServiceImpl.java
@@ -0,0 +1,82 @@
+package com.toanyone.store.application.service;
+
+import com.toanyone.store.common.util.PhoneNumberUtils;
+import com.toanyone.store.domain.exception.StoreException;
+import com.toanyone.store.domain.model.Store;
+import com.toanyone.store.domain.repository.StoreRepository;
+import com.toanyone.store.domain.service.StoreService;
+import com.toanyone.store.infrastructure.client.HubClient;
+import com.toanyone.store.infrastructure.client.dto.HubResponseDto;
+import com.toanyone.store.presentation.dto.SingleResponse;
+import com.toanyone.store.presentation.dto.StoreCreateRequestDto;
+import com.toanyone.store.presentation.dto.StoreCreateResponseDto;
+import com.toanyone.store.presentation.dto.StoreFindResponseDto;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+@Service
+@RequiredArgsConstructor
+@Transactional(readOnly = true)
+public class StoreServiceImpl implements StoreService {
+    private final StoreRepository storeRepository;
+    private final HubClient hubClient;
+
+    @Override
+    @Transactional
+    public StoreCreateResponseDto createStore(StoreCreateRequestDto storeCreateRequestDto) {
+        validateHubExists(storeCreateRequestDto.getHubId()); // 등록한 허브가 존재하는 허브인지 확인
+        validateStoreNameNotExists(storeCreateRequestDto); // 등록할 업체 이름 중복 확인
+
+        Store store = Store.create(
+                storeCreateRequestDto.getStoreName(),
+                storeCreateRequestDto.getHubId(),
+                storeCreateRequestDto.getDetailAddress(),
+                storeCreateRequestDto.getLocation(),
+                storeCreateRequestDto.getStoreType(),
+                PhoneNumberUtils.normalizePhoneNumber(storeCreateRequestDto.getTelephone()) // 전화번호에서 "-" 제거
+        );
+
+        storeRepository.save(store);
+        return new StoreCreateResponseDto(store.getId());
+    }
+
+    @Override
+    public StoreFindResponseDto findOne(Long storeId) {
+        Store findStore = validateStoreExists(storeId);
+        HubResponseDto hubResponseDto = validateHubExists(findStore.getHubId());
+        return StoreFindResponseDto.of(findStore, hubResponseDto.getHubId(), hubResponseDto.getHubName());
+
+    }
+
+    @Override
+    @Transactional
+    public void deleteStore(Long storeId) {
+        Store findStore = validateStoreExists(storeId); // 존재하는 업체인지 검증
+        findStore.delete(findStore.getId());
+    }
+
+    /**
+     * 등록하려는 업체명이 존재하는 지 검증
+     */
+    private void validateStoreNameNotExists(StoreCreateRequestDto storeCreateRequestDto) {
+        if (storeRepository.findByStoreName(storeCreateRequestDto.getStoreName()).isPresent())
+            throw new StoreException.StoreNameExistException("존재하는 업체명입니다.");
+    }
+
+    /**
+     * 존재하는 허브인지 검증
+     */
+    private HubResponseDto validateHubExists(Long hubId) {
+        SingleResponse<HubResponseDto> response = hubClient.getHubById(hubId);
+        if (response.getData() == null) throw new StoreException.HubNotFoundException("존재하지 않는 허브입니다.");
+        return response.getData();
+    }
+
+    /**
+     * 존재하는 업체인지 검증
+     */
+    private Store validateStoreExists(Long storeId) {
+        return storeRepository.findById(storeId).orElseThrow(() -> new StoreException.StoreNotFoundException("해당 업체를 찾을 수 없습니다."));
+    }
+}
\ No newline at end of file
diff --git a/store/src/main/java/com/toanyone/store/common/util/PhoneNumberUtils.java b/store/src/main/java/com/toanyone/store/common/util/PhoneNumberUtils.java
new file mode 100644
index 0000000..24273bb
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/common/util/PhoneNumberUtils.java
@@ -0,0 +1,31 @@
+package com.toanyone.store.common.util;
+
+import lombok.AccessLevel;
+import lombok.NoArgsConstructor;
+
+@NoArgsConstructor(access = AccessLevel.PROTECTED)
+public class PhoneNumberUtils {
+    public static String normalizePhoneNumber(String phoneNumber) {
+        return phoneNumber.replaceAll("[^0-9+]", "");
+    }
+
+    public static String formatPhoneNumber(String phoneNumber) {
+        if (phoneNumber.startsWith("+82")) {  // 한국 국제번호
+            phoneNumber = phoneNumber.replaceFirst("\\+82", "0"); // +82 → 0으로 변환
+        }
+
+        if (phoneNumber.startsWith("02")) {  // 서울 지역번호
+            if (phoneNumber.length() == 9) { // 02-XXX-XXXX
+                return phoneNumber.replaceFirst("(\\d{2})(\\d{3})(\\d{4})", "$1-$2-$3");
+            } else if (phoneNumber.length() == 10) { // 02-XXXX-XXXX
+                return phoneNumber.replaceFirst("(\\d{2})(\\d{4})(\\d{4})", "$1-$2-$3");
+            }
+        } else if (phoneNumber.length() == 10) {  // 일반 지역번호 (031, 051 등)
+            return phoneNumber.replaceFirst("(\\d{3})(\\d{3})(\\d{4})", "$1-$2-$3");
+        } else if (phoneNumber.length() == 11) {  // 휴대폰 번호
+            return phoneNumber.replaceFirst("(\\d{3})(\\d{4})(\\d{4})", "$1-$2-$3");
+        }
+
+        return phoneNumber; // 변환 불가능하면 원본 반환
+    }
+}
\ No newline at end of file
diff --git a/store/src/main/java/com/toanyone/store/domain/exception/CustomException.java b/store/src/main/java/com/toanyone/store/domain/exception/CustomException.java
new file mode 100644
index 0000000..2c28097
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/domain/exception/CustomException.java
@@ -0,0 +1,8 @@
+package com.toanyone.store.domain.exception;
+
+import lombok.AllArgsConstructor;
+
+@AllArgsConstructor
+public class CustomException extends RuntimeException {
+    private String message;
+}
diff --git a/store/src/main/java/com/toanyone/store/domain/exception/StoreException.java b/store/src/main/java/com/toanyone/store/domain/exception/StoreException.java
new file mode 100644
index 0000000..49703fd
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/domain/exception/StoreException.java
@@ -0,0 +1,31 @@
+package com.toanyone.store.domain.exception;
+
+public class StoreException extends CustomException{
+    public StoreException(String message) {
+        super(message);
+    }
+
+    public static class HubNotFoundException extends StoreException {
+        public HubNotFoundException(String message) {
+            super(message);
+        }
+    }
+
+    public static class StoreNameExistException extends StoreException {
+        public StoreNameExistException(String message) {
+            super(message);
+        }
+    }
+
+    public static class StoreNotFoundException extends StoreException {
+        public StoreNotFoundException(String message) {
+            super(message);
+        }
+    }
+
+    public static class StoreAlreadyDeletedException extends StoreException {
+        public StoreAlreadyDeletedException(String message) {
+            super(message);
+        }
+    }
+}
diff --git a/store/src/main/java/com/toanyone/store/domain/model/BaseEntity.java b/store/src/main/java/com/toanyone/store/domain/model/BaseEntity.java
index 06a6705..74ccccb 100644
--- a/store/src/main/java/com/toanyone/store/domain/model/BaseEntity.java
+++ b/store/src/main/java/com/toanyone/store/domain/model/BaseEntity.java
@@ -1,5 +1,6 @@
 package com.toanyone.store.domain.model;
 
+import com.toanyone.store.domain.exception.StoreException;
 import jakarta.persistence.Column;
 import jakarta.persistence.EntityListeners;
 import jakarta.persistence.MappedSuperclass;
@@ -34,7 +35,11 @@ public abstract class BaseEntity {
     private Long deletedBy; // 삭제자ID
 
     /**
-     * 엔티티 삭제 처리 로직 구현 해야 함.
+     * 엔티티 삭제할 경우 호출하기
      */
-
+    public void delete(Long id) {
+        if (this.deletedAt != null) throw new StoreException.StoreAlreadyDeletedException("이미 삭제된 엔티티입니다.");
+        this.deletedAt = LocalDateTime.now();
+        this.deletedBy = id;
+    }
 }
diff --git a/store/src/main/java/com/toanyone/store/domain/model/Address.java b/store/src/main/java/com/toanyone/store/domain/model/DetailAddress.java
similarity index 93%
rename from store/src/main/java/com/toanyone/store/domain/model/Address.java
rename to store/src/main/java/com/toanyone/store/domain/model/DetailAddress.java
index 8c343db..2050f79 100644
--- a/store/src/main/java/com/toanyone/store/domain/model/Address.java
+++ b/store/src/main/java/com/toanyone/store/domain/model/DetailAddress.java
@@ -11,7 +11,7 @@
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 @AllArgsConstructor
 @Getter
-public class Address {
+public class DetailAddress {
 
     @Column(nullable = false)
     private String detailAddress; // 상세 주소
diff --git a/store/src/main/java/com/toanyone/store/domain/model/Store.java b/store/src/main/java/com/toanyone/store/domain/model/Store.java
index 28e7493..338cf89 100644
--- a/store/src/main/java/com/toanyone/store/domain/model/Store.java
+++ b/store/src/main/java/com/toanyone/store/domain/model/Store.java
@@ -2,9 +2,11 @@
 
 import jakarta.persistence.*;
 import lombok.AccessLevel;
+import lombok.Getter;
 import lombok.NoArgsConstructor;
 
 @Entity
+@Getter
 @NoArgsConstructor(access = AccessLevel.PROTECTED)
 @Table(name = "P_STORE")
 public class Store extends BaseEntity {
@@ -24,7 +26,10 @@ public class Store extends BaseEntity {
     private Long hubId; // 업체가 등록된 허브의 ID
 
     @Column(nullable = false)
-    private Address address; // 상세주소
+    private DetailAddress detailAddress; // 상세주소
+
+    @Column(nullable = false)
+    private String telephone; // 전화번호
 
     @Column(nullable = false)
     private Location location; // 위도, 경도
@@ -32,13 +37,14 @@ public class Store extends BaseEntity {
     /**
      * 업체를 생성하는 정적 팩토리 메서드
      */
-    public static Store create(String storeName, Long hubId, Address detailAddress, Location location, StoreType storeType) {
+    public static Store create(String storeName, Long hubId, DetailAddress detailAddress, Location location, StoreType storeType, String telephone) {
         Store store = new Store();
         store.storeType = storeType;
         store.storeName = storeName;
         store.hubId = hubId;
-        store.address = detailAddress;
+        store.detailAddress = detailAddress;
         store.location = location;
+        store.telephone = telephone;
         return store;
     }
 }
diff --git a/store/src/main/java/com/toanyone/store/domain/model/StoreType.java b/store/src/main/java/com/toanyone/store/domain/model/StoreType.java
index 32f9d8f..cc0a1b7 100644
--- a/store/src/main/java/com/toanyone/store/domain/model/StoreType.java
+++ b/store/src/main/java/com/toanyone/store/domain/model/StoreType.java
@@ -1,5 +1,8 @@
 package com.toanyone.store.domain.model;
 
+import lombok.Getter;
+
+@Getter
 public enum StoreType {
     PRODUCER, CONSUMER;
 }
diff --git a/store/src/main/java/com/toanyone/store/domain/repository/StoreRepository.java b/store/src/main/java/com/toanyone/store/domain/repository/StoreRepository.java
new file mode 100644
index 0000000..fe124cd
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/domain/repository/StoreRepository.java
@@ -0,0 +1,14 @@
+package com.toanyone.store.domain.repository;
+
+import com.toanyone.store.domain.model.Store;
+
+import java.util.Optional;
+
+public interface StoreRepository {
+
+    Store save(Store store);
+
+    Optional<Store> findById(Long id);
+
+    Optional<Store> findByStoreName(String name);
+}
diff --git a/store/src/main/java/com/toanyone/store/domain/service/StoreService.java b/store/src/main/java/com/toanyone/store/domain/service/StoreService.java
new file mode 100644
index 0000000..6ce8806
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/domain/service/StoreService.java
@@ -0,0 +1,14 @@
+package com.toanyone.store.domain.service;
+
+import com.toanyone.store.presentation.dto.StoreCreateRequestDto;
+import com.toanyone.store.presentation.dto.StoreCreateResponseDto;
+import com.toanyone.store.presentation.dto.StoreFindResponseDto;
+import jakarta.validation.Valid;
+
+public interface StoreService {
+    StoreCreateResponseDto createStore(StoreCreateRequestDto storeCreateRequestDto);
+
+    StoreFindResponseDto findOne(Long storeId);
+
+    void deleteStore(Long storeId);
+}
diff --git a/store/src/main/java/com/toanyone/store/infrastructure/client/HubClient.java b/store/src/main/java/com/toanyone/store/infrastructure/client/HubClient.java
new file mode 100644
index 0000000..c23cc35
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/infrastructure/client/HubClient.java
@@ -0,0 +1,13 @@
+package com.toanyone.store.infrastructure.client;
+
+import com.toanyone.store.infrastructure.client.dto.HubResponseDto;
+import com.toanyone.store.presentation.dto.SingleResponse;
+import org.springframework.cloud.openfeign.FeignClient;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+
+@FeignClient(name = "hub-service", url = "http://localhost:9000")
+public interface HubClient {
+    @GetMapping("/hubs/{hubId}")
+    SingleResponse<HubResponseDto> getHubById(@PathVariable("hubId") Long hubId);
+}
\ No newline at end of file
diff --git a/store/src/main/java/com/toanyone/store/infrastructure/client/dto/HubResponseDto.java b/store/src/main/java/com/toanyone/store/infrastructure/client/dto/HubResponseDto.java
new file mode 100644
index 0000000..558504a
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/infrastructure/client/dto/HubResponseDto.java
@@ -0,0 +1,15 @@
+package com.toanyone.store.infrastructure.client.dto;
+
+import com.toanyone.store.domain.model.DetailAddress;
+import com.toanyone.store.domain.model.Location;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
+@AllArgsConstructor
+public class HubResponseDto {
+    private Long hubId;
+    private String hubName;
+    private DetailAddress detailAddress;
+    private Location location;
+}
diff --git a/store/src/main/java/com/toanyone/store/infrastructure/repository/JpaStoreRepository.java b/store/src/main/java/com/toanyone/store/infrastructure/repository/JpaStoreRepository.java
new file mode 100644
index 0000000..a3550a9
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/infrastructure/repository/JpaStoreRepository.java
@@ -0,0 +1,10 @@
+package com.toanyone.store.infrastructure.repository;
+
+import com.toanyone.store.domain.model.Store;
+import com.toanyone.store.domain.repository.StoreRepository;
+import org.springframework.data.jpa.repository.JpaRepository;
+
+import java.util.Optional;
+
+public interface JpaStoreRepository extends JpaRepository<Store, Long>, StoreRepository, StoreRepositoryCustom {
+}
diff --git a/store/src/main/java/com/toanyone/store/infrastructure/repository/StoreRepositoryCustom.java b/store/src/main/java/com/toanyone/store/infrastructure/repository/StoreRepositoryCustom.java
new file mode 100644
index 0000000..ef6375c
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/infrastructure/repository/StoreRepositoryCustom.java
@@ -0,0 +1,4 @@
+package com.toanyone.store.infrastructure.repository;
+
+public interface StoreRepositoryCustom {
+}
diff --git a/store/src/main/java/com/toanyone/store/infrastructure/repository/StoreRepositoryImpl.java b/store/src/main/java/com/toanyone/store/infrastructure/repository/StoreRepositoryImpl.java
new file mode 100644
index 0000000..ff1fa44
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/infrastructure/repository/StoreRepositoryImpl.java
@@ -0,0 +1,10 @@
+package com.toanyone.store.infrastructure.repository;
+
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Repository;
+
+@Repository
+@RequiredArgsConstructor
+public class StoreRepositoryImpl implements StoreRepositoryCustom {
+
+}
diff --git a/store/src/main/java/com/toanyone/store/presentation/controller/StoreController.java b/store/src/main/java/com/toanyone/store/presentation/controller/StoreController.java
new file mode 100644
index 0000000..25f3802
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/presentation/controller/StoreController.java
@@ -0,0 +1,42 @@
+package com.toanyone.store.presentation.controller;
+
+import com.toanyone.store.domain.service.StoreService;
+import com.toanyone.store.presentation.dto.SingleResponse;
+import com.toanyone.store.presentation.dto.StoreCreateRequestDto;
+import com.toanyone.store.presentation.dto.StoreCreateResponseDto;
+import com.toanyone.store.presentation.dto.StoreFindResponseDto;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.http.ResponseEntity;
+import org.springframework.web.bind.annotation.*;
+import jakarta.validation.Valid;
+
+@RestController
+@RequiredArgsConstructor
+@Slf4j
+@RequestMapping("/stores")
+public class StoreController {
+    private final StoreService storeService;
+
+    @PostMapping
+    public ResponseEntity createStore(@RequestBody @Valid StoreCreateRequestDto storeCreateRequestDto) {
+        log.debug("Create Store: {}", storeCreateRequestDto);
+        StoreCreateResponseDto storeCreateResponseDto = storeService.createStore(storeCreateRequestDto);
+        return ResponseEntity.ok(SingleResponse.success(storeCreateResponseDto));
+    }
+
+    @GetMapping("/{storeId}")
+    public ResponseEntity findStore(@PathVariable Long storeId) {
+        log.debug("Find Store: {}", storeId);
+        StoreFindResponseDto storeFindResponseDto = storeService.findOne(storeId);
+        return ResponseEntity.ok(SingleResponse.success(storeFindResponseDto));
+    }
+
+    @DeleteMapping("/{storeId}")
+    public ResponseEntity deleteStore(@PathVariable Long storeId) {
+        log.debug("Delete Store: {}", storeId);
+        storeService.deleteStore(storeId);
+        return ResponseEntity.noContent().build();
+    }
+
+}
diff --git a/store/src/main/java/com/toanyone/store/presentation/dto/MultiResponse.java b/store/src/main/java/com/toanyone/store/presentation/dto/MultiResponse.java
new file mode 100644
index 0000000..ec294cf
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/presentation/dto/MultiResponse.java
@@ -0,0 +1,66 @@
+package com.toanyone.store.presentation.dto;
+
+import lombok.Getter;
+import org.springframework.data.domain.Page;
+
+import java.util.List;
+
+@Getter
+public class MultiResponse<T> {
+    private final List<T> data;
+    private final int count;
+    private final int totalPages;
+    private final long totalElements;
+    private final int currentPage;
+    private final int pageSize;
+    private final String errorMessage;
+    private final String errorCode;
+
+    // 성공 응답
+    public MultiResponse(List<T> data) {
+        this.data = data;
+        this.count = data.size();
+        this.totalPages = 1;
+        this.totalElements = data.size();
+        this.currentPage = 0;
+        this.pageSize = data.size();
+        this.errorMessage = null;
+        this.errorCode = null;
+    }
+
+    // 성공 응답 (페이징)
+    public MultiResponse(Page<T> page) {
+        this.data = page.getContent();
+        this.count = page.getNumberOfElements();
+        this.totalPages = page.getTotalPages();
+        this.totalElements = page.getTotalElements();
+        this.currentPage = page.getNumber();
+        this.pageSize = page.getSize();
+        this.errorMessage = null;
+        this.errorCode = null;
+    }
+
+    // 에러 응답
+    public MultiResponse(String errorMessage, String errorCode) {
+        this.data = null;
+        this.count = 0;
+        this.totalPages = 0;
+        this.totalElements = 0;
+        this.currentPage = 0;
+        this.pageSize = 0;
+        this.errorMessage = errorMessage;
+        this.errorCode = errorCode;
+    }
+
+    public static <T> MultiResponse<T> success(List<T> data) {
+        return new MultiResponse<>(data);
+    }
+
+    public static <T> MultiResponse<T> success(Page<T> page) {
+        return new MultiResponse<>(page);
+    }
+
+    public static <T> MultiResponse<T> error(String errorMessage, String errorCode) {
+        return new MultiResponse<>(errorMessage, errorCode);
+    }
+}
diff --git a/store/src/main/java/com/toanyone/store/presentation/dto/SingleResponse.java b/store/src/main/java/com/toanyone/store/presentation/dto/SingleResponse.java
new file mode 100644
index 0000000..16b02ca
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/presentation/dto/SingleResponse.java
@@ -0,0 +1,32 @@
+package com.toanyone.store.presentation.dto;
+
+import lombok.Getter;
+
+@Getter
+public class SingleResponse<T> {
+    private final T data;
+    private final String errorMessage;
+    private final String errorCode;
+
+    //성공 응답
+    public SingleResponse(T data) {
+        this.data = data;
+        this.errorMessage = null;
+        this.errorCode = null;
+    }
+
+    //에러응답
+    public SingleResponse(String errorMessage, String errorCode) {
+        this.data = null;
+        this.errorMessage = errorMessage;
+        this.errorCode = errorCode;
+    }
+
+    public static <T> SingleResponse<T> success(T data) {
+        return new SingleResponse<>(data);
+    }
+
+    public static <T> SingleResponse<T> error(String errorMessage, String errorCode) {
+        return new SingleResponse<>(errorMessage, errorCode);
+    }
+}
diff --git a/store/src/main/java/com/toanyone/store/presentation/dto/StoreCreateRequestDto.java b/store/src/main/java/com/toanyone/store/presentation/dto/StoreCreateRequestDto.java
new file mode 100644
index 0000000..94e5271
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/presentation/dto/StoreCreateRequestDto.java
@@ -0,0 +1,30 @@
+package com.toanyone.store.presentation.dto;
+
+import com.toanyone.store.domain.model.DetailAddress;
+import com.toanyone.store.domain.model.Location;
+import com.toanyone.store.domain.model.StoreType;
+import jakarta.validation.constraints.NotBlank;
+import jakarta.validation.constraints.NotNull;
+import jakarta.validation.constraints.Size;
+import lombok.AccessLevel;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+import lombok.NoArgsConstructor;
+
+@Getter
+@AllArgsConstructor
+public class StoreCreateRequestDto {
+    @NotBlank
+    @Size(max = 30)
+    private String storeName;
+    @NotNull
+    private StoreType storeType;
+    @NotNull
+    private Location location;
+    @NotNull
+    private DetailAddress detailAddress;
+    @NotBlank
+    private Long hubId;
+    @NotBlank
+    private String telephone;
+}
diff --git a/store/src/main/java/com/toanyone/store/presentation/dto/StoreCreateResponseDto.java b/store/src/main/java/com/toanyone/store/presentation/dto/StoreCreateResponseDto.java
new file mode 100644
index 0000000..e1f5d16
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/presentation/dto/StoreCreateResponseDto.java
@@ -0,0 +1,9 @@
+package com.toanyone.store.presentation.dto;
+
+import lombok.*;
+
+@Getter
+@AllArgsConstructor
+public class StoreCreateResponseDto {
+    private Long StoreId;
+}
diff --git a/store/src/main/java/com/toanyone/store/presentation/dto/StoreFindResponseDto.java b/store/src/main/java/com/toanyone/store/presentation/dto/StoreFindResponseDto.java
new file mode 100644
index 0000000..b2f0c6d
--- /dev/null
+++ b/store/src/main/java/com/toanyone/store/presentation/dto/StoreFindResponseDto.java
@@ -0,0 +1,33 @@
+package com.toanyone.store.presentation.dto;
+
+import com.toanyone.store.common.util.PhoneNumberUtils;
+import com.toanyone.store.domain.model.DetailAddress;
+import com.toanyone.store.domain.model.Location;
+import com.toanyone.store.domain.model.Store;
+import com.toanyone.store.domain.model.StoreType;
+import lombok.*;
+
+@Getter
+public class StoreFindResponseDto {
+    private Long storeId;
+    private String storeName;
+    private StoreType storeType;
+    private Location location;
+    private DetailAddress detailAddress;
+    private Long hubId;
+    private String hubName;
+    private String telephone;
+
+    public static StoreFindResponseDto of(Store store, Long hubId, String hubName) {
+        StoreFindResponseDto storeFindResponseDto = new StoreFindResponseDto();
+        storeFindResponseDto.storeId = store.getId();
+        storeFindResponseDto.storeName = store.getStoreName();
+        storeFindResponseDto.storeType = store.getStoreType();
+        storeFindResponseDto.location = store.getLocation();
+        storeFindResponseDto.detailAddress = store.getDetailAddress();
+        storeFindResponseDto.hubName = hubName;
+        storeFindResponseDto.telephone = PhoneNumberUtils.formatPhoneNumber(store.getTelephone()); // 전화번호 "-" 추가
+        storeFindResponseDto.hubId = hubId;
+        return storeFindResponseDto;
+    }
+}
diff --git a/store/src/test/java/com/toanyone/store/application/service/StoreServiceImplTest.java b/store/src/test/java/com/toanyone/store/application/service/StoreServiceImplTest.java
new file mode 100644
index 0000000..a6bf0fa
--- /dev/null
+++ b/store/src/test/java/com/toanyone/store/application/service/StoreServiceImplTest.java
@@ -0,0 +1,105 @@
+package com.toanyone.store.application.service;
+
+import com.toanyone.store.domain.model.DetailAddress;
+import com.toanyone.store.domain.model.Location;
+import com.toanyone.store.domain.model.Store;
+import com.toanyone.store.domain.model.StoreType;
+import com.toanyone.store.domain.repository.StoreRepository;
+import com.toanyone.store.domain.service.StoreService;
+import com.toanyone.store.infrastructure.client.HubClient;
+import com.toanyone.store.presentation.dto.SingleResponse;
+import com.toanyone.store.presentation.dto.StoreCreateRequestDto;
+import com.toanyone.store.presentation.dto.StoreCreateResponseDto;
+import com.toanyone.store.presentation.dto.StoreFindResponseDto;
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.mockito.InjectMocks;
+import org.mockito.Mock;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.boot.test.mock.mockito.MockBean;
+import org.springframework.transaction.annotation.Transactional;
+import com.toanyone.store.infrastructure.client.dto.HubResponseDto;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Optional;
+
+import static org.assertj.core.api.Assertions.*;
+import static org.mockito.Mockito.*;
+
+
+@SpringBootTest
+@Transactional
+class StoreServiceImplTest {
+
+    @Autowired
+    StoreRepository storeRepository;
+
+    @Autowired  // @InjectMocks → @Autowired
+    private StoreService storeService;
+
+    @MockBean  // @Mock 대신 @MockBean 사용
+    private HubClient hubClient;
+
+
+    @Test
+    @DisplayName("업체 생성 테스트")
+    void StoreCreateTest() {
+        //given
+        HubResponseDto mockHubResponse = new HubResponseDto(1L, "허브 A", new DetailAddress("a동 102동"),
+                new Location(new BigDecimal("123.1234567").setScale(7, RoundingMode.HALF_UP), new BigDecimal("123.1234567").setScale(7, RoundingMode.HALF_UP)));
+        when(hubClient.getHubById(1L)).thenReturn(SingleResponse.success(mockHubResponse));
+        
+        StoreCreateRequestDto requestDto = new StoreCreateRequestDto(
+                "찬이네 서울 정육점", StoreType.PRODUCER,
+                new Location(new BigDecimal("123.1234567").setScale(7, RoundingMode.HALF_UP), new BigDecimal("123.1234567").setScale(7, RoundingMode.HALF_UP)),
+                new DetailAddress("102동 1201호"), 1L, "02-123-4567");
+
+        //then
+        StoreCreateResponseDto storeCreateResponseDto = storeService.createStore(requestDto);
+        Store findStore = storeRepository.findById(storeCreateResponseDto.getStoreId()).orElseThrow(RuntimeException::new);
+
+        assertThat(storeCreateResponseDto.getStoreId()).isEqualTo(findStore.getId());
+    }
+
+    @Test
+    @DisplayName("업체 단건 조회 테스트")
+    void storeFindOneTest(){
+        // given
+        HubResponseDto mockHubResponse = new HubResponseDto(1L, "허브 A", new DetailAddress("a동 102동"),
+                new Location(new BigDecimal("123.1234567").setScale(7, RoundingMode.HALF_UP), new BigDecimal("123.1234567").setScale(7, RoundingMode.HALF_UP)));
+        when(hubClient.getHubById(1L)).thenReturn(SingleResponse.success(mockHubResponse));
+
+        StoreCreateRequestDto requestDto = new StoreCreateRequestDto(
+                "찬이네 서울 정육점", StoreType.PRODUCER,
+                new Location(new BigDecimal("123.1234567").setScale(7, RoundingMode.HALF_UP), new BigDecimal("123.1234567").setScale(7, RoundingMode.HALF_UP)),
+                new DetailAddress("102동 1201호"), 1L, "02-123-4567");
+        StoreCreateResponseDto storeCreateResponseDto = storeService.createStore(requestDto);
+
+        // when
+        StoreFindResponseDto response = storeService.findOne(storeCreateResponseDto.getStoreId());
+
+        // then
+        assertThat(response.getStoreName()).isEqualTo("찬이네 서울 정육점");
+        assertThat(response.getHubName()).isEqualTo("허브 A");
+    }
+
+    @Test
+    @DisplayName("업체 삭제 테스트")
+    void storeDeleteTest(){
+        // given
+        Store savedStore = storeRepository.save(Store.create("찬이네 서울 정육점", 1L, new DetailAddress("102동 1201호"),
+                new Location(new BigDecimal("123.1234567").setScale(7, RoundingMode.HALF_UP), new BigDecimal("123.1234567").setScale(7, RoundingMode.HALF_UP)),
+                StoreType.PRODUCER, "02-123-4567"));
+
+        // when
+        savedStore.delete(savedStore.getId());
+        Store findStore = storeRepository.findById(savedStore.getId()).get();
+
+        // then
+        assertThat(findStore.getDeletedAt()).isNotNull();
+    }
+}
\ No newline at end of file
diff --git a/store/src/test/java/com/toanyone/store/common/util/PhoneNumberUtilsTest.java b/store/src/test/java/com/toanyone/store/common/util/PhoneNumberUtilsTest.java
new file mode 100644
index 0000000..654dc2c
--- /dev/null
+++ b/store/src/test/java/com/toanyone/store/common/util/PhoneNumberUtilsTest.java
@@ -0,0 +1,39 @@
+package com.toanyone.store.common.util;
+
+import org.assertj.core.api.Assertions;
+import org.junit.jupiter.api.DisplayName;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.transaction.annotation.Transactional;
+
+import static org.assertj.core.api.Assertions.*;
+
+
+@SpringBootTest
+@Transactional
+class PhoneNumberUtilsTest {
+
+    @Test
+    @DisplayName("PhoneNumberUtils 전화번호 -제거 테스트")
+    void TelValidationTest() {
+        String normalizePhoneNumber1 = PhoneNumberUtils.normalizePhoneNumber("010-1234-5678");
+        String normalizePhoneNumber2 = PhoneNumberUtils.normalizePhoneNumber("+82 10-1234-5678");
+        String normalizePhoneNumber3 = PhoneNumberUtils.normalizePhoneNumber("02-123-4567");
+
+        assertThat(normalizePhoneNumber1).isEqualTo("01012345678");
+        assertThat(normalizePhoneNumber2).isEqualTo("+821012345678");
+        assertThat(normalizePhoneNumber3).isEqualTo("021234567");
+    }
+
+    @Test
+    @DisplayName("PhoneNumberUtils 전화번호 -추가 테스트")
+    void TelValidationTest2() {
+        String normalizePhoneNumber1 = PhoneNumberUtils.formatPhoneNumber("01012345678");
+        String normalizePhoneNumber2 = PhoneNumberUtils.formatPhoneNumber("+821012345678");
+        String normalizePhoneNumber3 = PhoneNumberUtils.formatPhoneNumber("021234567");
+
+        assertThat(normalizePhoneNumber1).isEqualTo("010-1234-5678");
+        assertThat(normalizePhoneNumber2).isEqualTo("010-1234-5678");
+        assertThat(normalizePhoneNumber3).isEqualTo("02-123-4567");
+    }
+}
\ No newline at end of file


Reviews:
user: login: woo-lala
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T13:21:36Z
user: login: venus-y
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-15T17:44:09Z

Review comments:
diff_hunk: @@ -0,0 +1,15 @@
+package com.toanyone.store.infrastructure.client.dto;
+
+import com.toanyone.store.domain.model.DetailAddress;
+import com.toanyone.store.domain.model.Location;
+import lombok.AllArgsConstructor;
+import lombok.Data;
+
+@Data
path: store/src/main/java/com/toanyone/store/infrastructure/client/dto/HubResponseDto.java
user: login: woo-lala
body: Data 애너테이션에 setter가 포함된다고 알고 있습니다. 한번 확인해 보시면 좋을 것 같습니다!
created_at: 2025-03-15T13:21:36Z
author_association: CONTRIBUTOR

PR comments:
user: login: venus-y
created_at: 2025-03-15T10:40:39Z
body: 작업하시느라 고생하셨습니다. 전체적으로 깔끔하게 잘 작성해주신 것 같습니다.
author_association: CONTRIBUTOR
user: login: woo-lala
created_at: 2025-03-15T13:22:25Z
body: 수고하셨습니다!
author_association: CONTRIBUTOR