diff --git a/appspec.yml b/appspec.yml new file mode 100644 index 0000000..0305a74 --- /dev/null +++ b/appspec.yml @@ -0,0 +1,15 @@ +version: 0.0 +os: linux + +files: + - source: / + destination: /home/ubuntu/couphone +permissions: + - object: /home/ubuntu/couphone/ + owner: ubuntu + group: ubuntu +hooks: + AfterInstall: + - location: scripts/deploy.sh + timeout: 60 + runas: ubuntu \ No newline at end of file diff --git a/build.gradle b/build.gradle index 929a1b9..b308e6b 100644 --- a/build.gradle +++ b/build.gradle @@ -26,7 +26,6 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'mysql:mysql-connector-java:8.0.28' implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0' - implementation 'org.projectlombok:lombok:1.18.22' testImplementation 'junit:junit:4.13.1' //JWT implementation 'org.springframework.boot:spring-boot-starter-security' @@ -44,6 +43,8 @@ dependencies { //Marvin for image resizing implementation 'com.github.downgoon:marvin:1.5.5' implementation 'com.github.downgoon:MarvinPlugins:1.5.5' + //json + implementation group: 'com.googlecode.json-simple', name: 'json-simple', version: '1.1.1' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 0000000..5d66d71 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,21 @@ + +REPOSITORY=/home/ubuntu/couphone +cd $REPOSITORY + +APP_NAME=couphone +JAR_NAME=$(ls $REPOSITORY/build/libs/ | grep 'SNAPSHOT.jar' | tail -n 1) +JAR_PATH=$REPOSITORY/build/libs/$JAR_NAME + +CURRENT_PID=$(pgrep -f $APP_NAME) + +if [ -z $CURRENT_PID ] +then + echo "> 종료할 애플리케이션이 없습니다." +else + echo "> kill -9 $CURRENT_PID" + kill -15 $CURRENT_PID + sleep 5 +fi + +echo "> Deploy - $JAR_PATH " +nohup java -jar $JAR_PATH > /dev/null 2> /dev/null < /dev/null & \ No newline at end of file diff --git a/settings.gradle b/settings.gradle index 5766ad8..928d4a6 100644 --- a/settings.gradle +++ b/settings.gradle @@ -1 +1 @@ -rootProject.name = 'Couphone-Server' +rootProject.name = 'couphone-server' diff --git a/src/main/java/com/example/couphoneserver/common/datatype/Coordinate.java b/src/main/java/com/example/couphoneserver/common/datatype/Coordinate.java new file mode 100644 index 0000000..44f22f7 --- /dev/null +++ b/src/main/java/com/example/couphoneserver/common/datatype/Coordinate.java @@ -0,0 +1,16 @@ +package com.example.couphoneserver.common.datatype; + +import lombok.Builder; +import lombok.Data; + +@Data +public class Coordinate { + private double longitude; + private double latitude; + + @Builder + public Coordinate(double longitude, double latitude) { + this.longitude = longitude; + this.latitude = latitude; + } +} diff --git a/src/main/java/com/example/couphoneserver/common/exception/StoreException.java b/src/main/java/com/example/couphoneserver/common/exception/StoreException.java new file mode 100644 index 0000000..d96c93d --- /dev/null +++ b/src/main/java/com/example/couphoneserver/common/exception/StoreException.java @@ -0,0 +1,19 @@ +package com.example.couphoneserver.common.exception; + +import com.example.couphoneserver.common.response.status.ResponseStatus; +import lombok.Getter; + +@Getter +public class StoreException extends RuntimeException { + private final ResponseStatus exceptionStatus; + + public StoreException(ResponseStatus exceptionStatus) { + super(exceptionStatus.getMessage()); + this.exceptionStatus = exceptionStatus; + } + + public StoreException(ResponseStatus exceptionStatus, String message) { + super(message); + this.exceptionStatus = exceptionStatus; + } +} diff --git a/src/main/java/com/example/couphoneserver/common/exception_handler/StoreExceptionControllerAdvice.java b/src/main/java/com/example/couphoneserver/common/exception_handler/StoreExceptionControllerAdvice.java new file mode 100644 index 0000000..3855bad --- /dev/null +++ b/src/main/java/com/example/couphoneserver/common/exception_handler/StoreExceptionControllerAdvice.java @@ -0,0 +1,23 @@ +package com.example.couphoneserver.common.exception_handler; + +import com.example.couphoneserver.common.exception.StoreException; +import com.example.couphoneserver.common.response.BaseErrorResponse; +import jakarta.annotation.Priority; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@Priority(0) +@RestControllerAdvice +public class StoreExceptionControllerAdvice { + + @ResponseStatus(HttpStatus.BAD_REQUEST) + @ExceptionHandler(StoreException.class) + public BaseErrorResponse handle_StoreException(StoreException e) { + return new BaseErrorResponse(e.getExceptionStatus(), e.getMessage()); + } + + + +} \ No newline at end of file diff --git a/src/main/java/com/example/couphoneserver/common/response/status/BaseExceptionResponseStatus.java b/src/main/java/com/example/couphoneserver/common/response/status/BaseExceptionResponseStatus.java index 01f958c..2fa2422 100644 --- a/src/main/java/com/example/couphoneserver/common/response/status/BaseExceptionResponseStatus.java +++ b/src/main/java/com/example/couphoneserver/common/response/status/BaseExceptionResponseStatus.java @@ -55,6 +55,13 @@ public enum BaseExceptionResponseStatus implements ResponseStatus { */ BRAND_NOT_FOUND(7000, HttpStatus.BAD_REQUEST.value(), "브랜드가 존재하지 않습니다."), DUPLICATE_BRAND_NAME(7001, HttpStatus.BAD_REQUEST.value(), "중복된 브랜드 이름이 존재합니다."), + /** + * 8000: 가게 오류 + */ + INVALID_STORE_VALUE(8000,HttpStatus.BAD_REQUEST.value(), "가게 등록에 유효하지 않은 정보입니다."), + COORDINATE_NOT_FOUND(8001,HttpStatus.BAD_REQUEST.value(), "좌표를 찾을 수 없습니다."), + DUPLICATE_STORE_NAME(8002,HttpStatus.BAD_REQUEST.value(), "중복된 지점이 존재합니다."), + NEARBY_STORE_NOT_FOUND(8003,HttpStatus.BAD_REQUEST.value(), "좌표 기준 주변 가게가 없습니다."), /** * 9000: 쿠폰 오류 diff --git a/src/main/java/com/example/couphoneserver/config/SecurityConfig.java b/src/main/java/com/example/couphoneserver/config/SecurityConfig.java index 63f7e99..786d1c4 100644 --- a/src/main/java/com/example/couphoneserver/config/SecurityConfig.java +++ b/src/main/java/com/example/couphoneserver/config/SecurityConfig.java @@ -53,7 +53,6 @@ public CustomAuthenticationProvider customAuthenticationProvider() throws Except return new CustomAuthenticationProvider(memberDetailService); } - // 비밀번호 암호화 관련 @Bean public BCryptPasswordEncoder bCryptPasswordEncoder() { diff --git a/src/main/java/com/example/couphoneserver/config/WebConfig.java b/src/main/java/com/example/couphoneserver/config/WebConfig.java index c5c65af..9ec91aa 100644 --- a/src/main/java/com/example/couphoneserver/config/WebConfig.java +++ b/src/main/java/com/example/couphoneserver/config/WebConfig.java @@ -21,7 +21,7 @@ public class WebConfig implements WebMvcConfigurer { public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(jwtAuthenticationInterceptor) .order(1) - .addPathPatterns("/auth", "/brands", "/users") + .addPathPatterns("/auth", "/brands", "/users","/stores") .excludePathPatterns("/auth/login"); } diff --git a/src/main/java/com/example/couphoneserver/controller/CategoryController.java b/src/main/java/com/example/couphoneserver/controller/CategoryController.java index e0ae1db..8fcd8a6 100644 --- a/src/main/java/com/example/couphoneserver/controller/CategoryController.java +++ b/src/main/java/com/example/couphoneserver/controller/CategoryController.java @@ -13,7 +13,10 @@ import io.swagger.v3.oas.annotations.tags.Tag; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; -import org.springframework.web.bind.annotation.*; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; import java.util.List; diff --git a/src/main/java/com/example/couphoneserver/controller/StoreController.java b/src/main/java/com/example/couphoneserver/controller/StoreController.java new file mode 100644 index 0000000..784860e --- /dev/null +++ b/src/main/java/com/example/couphoneserver/controller/StoreController.java @@ -0,0 +1,67 @@ +package com.example.couphoneserver.controller; + +import com.example.couphoneserver.common.annotation.NoAuth; +import com.example.couphoneserver.common.datatype.Coordinate; +import com.example.couphoneserver.common.exception.StoreException; +import com.example.couphoneserver.common.response.BaseResponse; +import com.example.couphoneserver.dto.store.*; +import com.example.couphoneserver.repository.mappingInterface.StoreInfoMapping; +import com.example.couphoneserver.service.StoreService; +import com.example.couphoneserver.utils.CoordinateConverter; +import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.tags.Tag; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.validation.BindingResult; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.*; + +import java.security.Principal; +import java.util.List; + +import static com.example.couphoneserver.common.response.status.BaseExceptionResponseStatus.INVALID_STORE_VALUE; +import static com.example.couphoneserver.utils.BindingResultUtils.getErrorMessages; + +@Tag(name = "store", description = "가게 관련 API 입니다.") +@Slf4j +@RestController +@RequiredArgsConstructor +@RequestMapping("/stores") +public class StoreController { + private final StoreService storeService; + private final CoordinateConverter coordinateConverter; + + @NoAuth + @PostMapping("") + @Operation(summary = "가게 등록", description = "Request Body에 브랜드 아이디, 매장명, 위도, 경도, 주소를 담아서 보내주세요!") + public BaseResponse postBrand(@Validated @RequestBody PostStoreRequest request, + BindingResult bindingResult){ + if (bindingResult.hasErrors()){ + throw new StoreException(INVALID_STORE_VALUE,getErrorMessages(bindingResult)); + } + return new BaseResponse<>(storeService.save(request)); + } + + @NoAuth + @PostMapping("/coordinate") + @Operation(summary = "좌표 변환", description = "Request Body에 주소를 담아 보내면 좌표를 반환합니다.") + public BaseResponse translateCoordinate(@Validated @RequestBody PostCoordinateRequest request, + BindingResult bindingResult){ + if (bindingResult.hasErrors()){ + throw new StoreException(INVALID_STORE_VALUE,getErrorMessages(bindingResult)); + } + return new BaseResponse<>(coordinateConverter.getCoordinate(request.getAddress())); + } + + @PostMapping("/nearby") + @Operation(summary = "좌표 중심 가게 반환", description = "Request Body에 좌표를 담아 보내면 주변 가게 리스트를 반환합니다.") + public BaseResponse> translateCoordinate(@Validated @RequestBody PostNearbyStoreRequest request, + Principal principal, + BindingResult bindingResult){ + if (bindingResult.hasErrors()){ + throw new StoreException(INVALID_STORE_VALUE,getErrorMessages(bindingResult)); + } + return new BaseResponse<>(storeService.findNearbyStores(principal,request)); + } + +} diff --git a/src/main/java/com/example/couphoneserver/domain/Address.java b/src/main/java/com/example/couphoneserver/domain/Address.java deleted file mode 100644 index e9e6a24..0000000 --- a/src/main/java/com/example/couphoneserver/domain/Address.java +++ /dev/null @@ -1,21 +0,0 @@ -package com.example.couphoneserver.domain; - -import jakarta.persistence.Embeddable; -import lombok.Getter; -import lombok.Setter; - -@Embeddable -@Getter -@Setter -public class Address { - private String city; - private String street; - private String zipcode; - public Address() { - } - public Address(String city, String street, String zipcode) { - this.city = city; - this.street = street; - this.zipcode = zipcode; - } -} \ No newline at end of file diff --git a/src/main/java/com/example/couphoneserver/domain/entity/Store.java b/src/main/java/com/example/couphoneserver/domain/entity/Store.java index 385b87d..705b2e3 100644 --- a/src/main/java/com/example/couphoneserver/domain/entity/Store.java +++ b/src/main/java/com/example/couphoneserver/domain/entity/Store.java @@ -1,24 +1,23 @@ package com.example.couphoneserver.domain.entity; -import com.example.couphoneserver.domain.Address; import com.example.couphoneserver.domain.StoreStatus; import jakarta.persistence.*; -import lombok.AccessLevel; -import lombok.Getter; -import lombok.NoArgsConstructor; +import lombok.*; + @Getter @NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @Table(name = "STORE") +@ToString public class Store extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "store_id") private Long id; private String name; - @Embedded - private Address address; // 불필요시 제거 + private String address; private Double longitude; private Double latitude; @@ -27,4 +26,15 @@ public class Store extends BaseTimeEntity { @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "brand_id") private Brand brand; // 해당 매장의 브랜드 + + @Builder + public Store(String name, String address, Double longitude, Double latitude, StoreStatus status, Brand brand) { + this.name = name; + this.address = address; + this.longitude = longitude; + this.latitude = latitude; + this.status = status; + this.brand = brand; + } + } diff --git a/src/main/java/com/example/couphoneserver/dto/store/PostCoordinateRequest.java b/src/main/java/com/example/couphoneserver/dto/store/PostCoordinateRequest.java new file mode 100644 index 0000000..465da1e --- /dev/null +++ b/src/main/java/com/example/couphoneserver/dto/store/PostCoordinateRequest.java @@ -0,0 +1,14 @@ +package com.example.couphoneserver.dto.store; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class PostCoordinateRequest { + @NotBlank(message = "address: {NotBlank}") + @Schema(example = "서울특별시 광진구 능동로 120") + private String address; +} diff --git a/src/main/java/com/example/couphoneserver/dto/store/PostNearbyStoreRequest.java b/src/main/java/com/example/couphoneserver/dto/store/PostNearbyStoreRequest.java new file mode 100644 index 0000000..b14eda2 --- /dev/null +++ b/src/main/java/com/example/couphoneserver/dto/store/PostNearbyStoreRequest.java @@ -0,0 +1,34 @@ +package com.example.couphoneserver.dto.store; + +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.annotation.Nullable; +import jakarta.validation.constraints.NotNull; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class PostNearbyStoreRequest { + private static final int radius = 500; + /* + **버튼 있을 경우 ⇒ 지름 1km** + + **버튼 없을 경우 ⇒ 반지름 1km** + */ + @NotNull(message = "longitude: {NotNull}") + @Schema(example = "207005.189144674") + private double longitude; + @NotNull(message = "latitude: {NotNull}") + @Schema(example = "449492.810069438") + private double latitude; + @NotNull(message = "is1km: {NotNull}") + @Schema(example="true",description = "반지름이 1km인 경우 true를 넣어주세요") + private Boolean is1km; + @Nullable + @Schema(description = "값을 넣어 보내지 마세요!") + private double distance; + + public void setDistance() { + this.distance = is1km?radius*2:radius; + } +} diff --git a/src/main/java/com/example/couphoneserver/dto/store/PostNearbyStoreResponse.java b/src/main/java/com/example/couphoneserver/dto/store/PostNearbyStoreResponse.java new file mode 100644 index 0000000..76c4e4f --- /dev/null +++ b/src/main/java/com/example/couphoneserver/dto/store/PostNearbyStoreResponse.java @@ -0,0 +1,25 @@ +package com.example.couphoneserver.dto.store; + +import com.example.couphoneserver.domain.entity.Brand; +import com.example.couphoneserver.dto.brand.GetBrandResponse; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Data; +import lombok.NoArgsConstructor; + +@Data +@NoArgsConstructor +public class PostNearbyStoreResponse { + private Long store_id; + private String name; + private Long brand_id; + private GetBrandResponse getBrandResponse; + private double distance; + + @Builder + public PostNearbyStoreResponse(Long store_id, String name, Long brand_id) { + this.store_id = store_id; + this.name = name; + this.brand_id = brand_id; + } +} diff --git a/src/main/java/com/example/couphoneserver/dto/store/PostStoreRequest.java b/src/main/java/com/example/couphoneserver/dto/store/PostStoreRequest.java new file mode 100644 index 0000000..da320ea --- /dev/null +++ b/src/main/java/com/example/couphoneserver/dto/store/PostStoreRequest.java @@ -0,0 +1,48 @@ +package com.example.couphoneserver.dto.store; + +import com.example.couphoneserver.domain.StoreStatus; +import com.example.couphoneserver.domain.entity.Brand; +import com.example.couphoneserver.domain.entity.Store; +import io.swagger.v3.oas.annotations.media.Schema; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; +import lombok.AllArgsConstructor; +import lombok.Data; +import lombok.NoArgsConstructor; +import org.hibernate.validator.constraints.Range; + +@Data +@NoArgsConstructor +@AllArgsConstructor +public class PostStoreRequest { + @NotNull(message = "bid: {NotNull}") + @Range(min=1,message="bid: 브랜드 아이디는 {min} 이상부터 가능합니다") + @Schema(example="1", description = "브랜드 등록 후 응답값으로 받은 브랜드 아이디를 넣어주세요.") + private Long bid; + + @NotBlank(message = "name: {NotBlank}") + @Size(min=1,max=100,message = "name: 가게 이름의 길이는 {min} 이상, {max} 이하 가능합니다") + @Schema(example="건국대학교 서울캠퍼스",description = "브랜드 이름+공백+지점명으로 보내주세요.") + private String name; + @NotBlank(message = "address: {NotBlank}") + @Schema(example = "서울특별시 광진구 능동로 120") + private String address; + @NotNull(message = "longitude: {NotNull}") + @Schema(example = "207005.189144674") + private Double longitude; + @NotNull(message = "latitude: {NotNull}") + @Schema(example = "449492.810069438") + private Double latitude; + + public Store toEntity(Brand brand) { + return Store.builder() + .name(name) + .address(address) + .longitude(longitude) + .latitude(latitude) + .status(StoreStatus.ACTIVE) + .brand(brand) + .build(); + } +} diff --git a/src/main/java/com/example/couphoneserver/dto/store/PostStoreResponse.java b/src/main/java/com/example/couphoneserver/dto/store/PostStoreResponse.java new file mode 100644 index 0000000..bce651a --- /dev/null +++ b/src/main/java/com/example/couphoneserver/dto/store/PostStoreResponse.java @@ -0,0 +1,16 @@ +package com.example.couphoneserver.dto.store; + +import io.swagger.v3.oas.annotations.media.Schema; +import lombok.Data; + + + +@Data +public class PostStoreResponse { + @Schema(example = "1", description = "회원 아이디") + private Long id; + + public PostStoreResponse(Long id) { + this.id = id; + } +} diff --git a/src/main/java/com/example/couphoneserver/repository/StoreRepository.java b/src/main/java/com/example/couphoneserver/repository/StoreRepository.java index b5f529c..534cd8e 100644 --- a/src/main/java/com/example/couphoneserver/repository/StoreRepository.java +++ b/src/main/java/com/example/couphoneserver/repository/StoreRepository.java @@ -1,7 +1,21 @@ package com.example.couphoneserver.repository; import com.example.couphoneserver.domain.entity.Store; +import com.example.couphoneserver.repository.mappingInterface.StoreInfoMapping; +import org.springframework.data.domain.Example; import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; + +import java.util.List; public interface StoreRepository extends JpaRepository { + boolean existsByName(String name); + + @Query(value = "select " + + "s1_0.store_id,s1_0.name,s1_0.brand_id,s1_0.longitude,s1_0.latitude " + + "from store s1_0 " + + "where s1_0.longitude between ?1 and ?2 " + + "and s1_0.latitude between ?3 and ?4", nativeQuery = true) + List findNearbyStores(double minX, double maxX, double minY, double maxY); + } diff --git a/src/main/java/com/example/couphoneserver/repository/mappingInterface/StoreInfoMapping.java b/src/main/java/com/example/couphoneserver/repository/mappingInterface/StoreInfoMapping.java new file mode 100644 index 0000000..7cadd48 --- /dev/null +++ b/src/main/java/com/example/couphoneserver/repository/mappingInterface/StoreInfoMapping.java @@ -0,0 +1,27 @@ +package com.example.couphoneserver.repository.mappingInterface; + +import com.example.couphoneserver.common.datatype.Coordinate; +import com.example.couphoneserver.dto.store.PostNearbyStoreResponse; + +public interface StoreInfoMapping { + Long getStore_id(); + String getName(); + Double getLongitude(); + Double getLatitude(); + Long getBrand_id(); + + default Coordinate translateCoordinate(){ + return Coordinate.builder() + .longitude(getLongitude()) + .latitude(getLatitude()) + .build(); + } + + default PostNearbyStoreResponse translateResponse(){ + return PostNearbyStoreResponse.builder() + .store_id(getStore_id()) + .name(getName()) + .brand_id(getBrand_id()) + .build(); + } +} diff --git a/src/main/java/com/example/couphoneserver/service/StoreService.java b/src/main/java/com/example/couphoneserver/service/StoreService.java new file mode 100644 index 0000000..cae4793 --- /dev/null +++ b/src/main/java/com/example/couphoneserver/service/StoreService.java @@ -0,0 +1,137 @@ +package com.example.couphoneserver.service; + +import com.example.couphoneserver.common.datatype.Coordinate; +import com.example.couphoneserver.common.exception.BrandException; +import com.example.couphoneserver.common.exception.StoreException; +import com.example.couphoneserver.domain.CouponItemStatus; +import com.example.couphoneserver.domain.entity.Brand; +import com.example.couphoneserver.domain.entity.CouponItem; +import com.example.couphoneserver.domain.entity.Store; +import com.example.couphoneserver.dto.brand.GetBrandResponse; +import com.example.couphoneserver.dto.store.PostNearbyStoreRequest; +import com.example.couphoneserver.dto.store.PostNearbyStoreResponse; +import com.example.couphoneserver.dto.store.PostStoreRequest; +import com.example.couphoneserver.dto.store.PostStoreResponse; +import com.example.couphoneserver.repository.BrandRepository; +import com.example.couphoneserver.repository.CouponItemRepository; +import com.example.couphoneserver.repository.StoreRepository; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.security.Principal; +import java.util.*; + +import static com.example.couphoneserver.common.response.status.BaseExceptionResponseStatus.BRAND_NOT_FOUND; +import static com.example.couphoneserver.common.response.status.BaseExceptionResponseStatus.DUPLICATE_STORE_NAME; + +@Slf4j +@Service +@Transactional(readOnly = true) +@RequiredArgsConstructor +public class StoreService { + private final StoreRepository storeRepository; + private final BrandRepository brandRepository; + private final CouponItemRepository couponItemRepository; + private final MemberService memberService; + + private static final int ELEMENT = 4; + + /* + 가게 등록 + */ + @Transactional + public PostStoreResponse save(PostStoreRequest request) { + //매장 브랜드 찾기 + Brand brandOfStore = validateBrand(request.getBid()); + //지점명 중복 확인 + validateStoreName(request); + //매장 등록 + Store store = storeRepository.save(request.toEntity(brandOfStore)); + return new PostStoreResponse(store.getId()); + } + + /* + 가게 조회 + */ + public List findNearbyStores(Principal principal,PostNearbyStoreRequest request){ + List storeList = getCandidateStoreList(request); + Collections.sort(storeList, new Comparator() { + @Override + public int compare(PostNearbyStoreResponse o1, PostNearbyStoreResponse o2) { + return o1.getDistance() > o2.getDistance()? 1: -1; + } + }); + log.info(String.valueOf(storeList.size())); + int numOfElement = storeList.size()>=ELEMENT?ELEMENT:storeList.size(); + + List resultList = storeList.subList(0,numOfElement); + + for (PostNearbyStoreResponse response: resultList) { + response.setGetBrandResponse(getGetBrandResponse(principal, response.getBrand_id())); + } + + return resultList; + } + + private List getCandidateStoreList(PostNearbyStoreRequest request) { + request.setDistance(); + double x = request.getLongitude(); + double y = request.getLatitude(); + double radius = request.getDistance(); + double minLongitude = x - radius; + double maxLongitude = x + radius; + double minLatitude = y - radius; + double maxLatitude = y + radius; + List StoreList = new ArrayList<>(); + storeRepository.findNearbyStores(minLongitude,maxLongitude,minLatitude,maxLatitude).stream().forEach(c -> { + PostNearbyStoreResponse response = c.translateResponse(); + Coordinate coordinate = c.translateCoordinate(); + response.setDistance(calculateDistance(x,y,coordinate)); + StoreList.add(response); + }); + return StoreList; + } + + public GetBrandResponse getGetBrandResponse(Principal principal, Long id) { + + // 멤버 ID + Long memberId = findMemberIdByPrincipal(principal); + + Brand brand = brandRepository.findById(id).get(); + + if (brand == null) throw new BrandException(BRAND_NOT_FOUND); + + CouponItem couponItem = couponItemRepository.findByMemberIdAndBrandIdAndStatus(memberId, id, CouponItemStatus.ACTIVE); + + if (couponItem == null) // 해당 브랜드에 쿠폰이 없을 경우 + return new GetBrandResponse(brand, 0); + return new GetBrandResponse(brand, couponItem.getStampCount()); + } + + private Long findMemberIdByPrincipal(Principal principal) { + String email = principal.getName(); + return memberService.findOneByEmail(email).getId(); + } + + private double calculateDistance(double x, double y, Coordinate coordinate) { + double distanceX = Math.abs(coordinate.getLongitude() - x); + double distanceY = Math.abs(coordinate.getLatitude() - y); + return Math.sqrt(distanceX*distanceX+distanceY*distanceY); + } + + private void validateStoreName(PostStoreRequest postStoreRequest) { +// log.info("[StoreService.validateStoreName]"); + if(storeRepository.existsByName(postStoreRequest.getName())) + throw new StoreException(DUPLICATE_STORE_NAME); + } + + private Brand validateBrand(Long brandId) { +// log.info("[StoreService.validateBrand]"); + Optional brand = brandRepository.findById(brandId); + if(brand.isEmpty()) throw new BrandException(BRAND_NOT_FOUND); + return brand.get(); + } + +} diff --git a/src/main/java/com/example/couphoneserver/utils/CoordinateConverter.java b/src/main/java/com/example/couphoneserver/utils/CoordinateConverter.java new file mode 100644 index 0000000..e0d0c1b --- /dev/null +++ b/src/main/java/com/example/couphoneserver/utils/CoordinateConverter.java @@ -0,0 +1,78 @@ +package com.example.couphoneserver.utils; + +import com.example.couphoneserver.common.datatype.Coordinate; +import com.example.couphoneserver.common.exception.StoreException; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.json.simple.JSONObject; +import org.json.simple.parser.JSONParser; +import org.json.simple.parser.ParseException; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.stereotype.Component; + +import java.io.BufferedReader; +import java.io.IOException; +import java.io.InputStreamReader; +import java.net.URL; +import java.net.URLEncoder; +import java.nio.charset.StandardCharsets; + +import static com.example.couphoneserver.common.response.status.BaseExceptionResponseStatus.COORDINATE_NOT_FOUND; + +@Slf4j +@Component +@RequiredArgsConstructor +public class CoordinateConverter { + //실제 환경에서는 실행이 되는데, 테스트 환경에서는 값이 주입되지 않는 상황 + private String apikey = "F224F68F-0102-3D4D-9467-84B239E77413"; + + private String searchType = "road"; + + private String epsg = "epsg:5181"; + /* + 도로명 주소 필수! + */ + public Coordinate getCoordinate(String address){ + + String searchAddr = address; + + StringBuilder sb = getURL(searchAddr); + + BufferedReader reader; + + try { + URL url = new URL(sb.toString()); + reader = new BufferedReader(new InputStreamReader(url.openStream(), StandardCharsets.UTF_8)); + + JSONParser jspa = new JSONParser(); + JSONObject jsob = (JSONObject) jspa.parse(reader); + JSONObject jsrs = (JSONObject) jsob.get("response"); + + JSONObject jsResult = (JSONObject) jsrs.get("result"); + JSONObject jspoitn = (JSONObject) jsResult.get("point"); + + String x = jspoitn.get("x").toString(); + String y = jspoitn.get("y").toString(); + + System.out.println(x); + System.out.println(y); + + return new Coordinate(Double.parseDouble(x),Double.parseDouble(y)); + } catch (StoreException | ParseException | IOException e) { + throw new StoreException(COORDINATE_NOT_FOUND,e.getMessage()); + } + } + + private StringBuilder getURL(String searchAddr) { + StringBuilder sb = new StringBuilder("https://api.vworld.kr/req/address"); + sb.append("?service=address"); + sb.append("&request=getCoord"); + sb.append("&format=json"); + sb.append("&crs=" + epsg); + sb.append("&key=" + apikey); + sb.append("&type=" + searchType); + sb.append("&simple=true"); + sb.append("&address=" + URLEncoder.encode(searchAddr, StandardCharsets.UTF_8)); + return sb; + } +} diff --git a/src/main/java/com/example/couphoneserver/utils/S3Uploader.java b/src/main/java/com/example/couphoneserver/utils/S3Uploader.java index 30476ea..14ac6e0 100644 --- a/src/main/java/com/example/couphoneserver/utils/S3Uploader.java +++ b/src/main/java/com/example/couphoneserver/utils/S3Uploader.java @@ -17,12 +17,10 @@ import org.apache.tomcat.util.http.fileupload.ByteArrayOutputStream; import org.marvinproject.image.transform.scale.Scale; import org.springframework.beans.factory.annotation.Value; -import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import org.springframework.web.multipart.MultipartFile; -import org.springframework.web.server.ResponseStatusException; import javax.imageio.ImageIO; import java.awt.image.BufferedImage; diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index e195131..8945f06 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,6 +20,7 @@ logging: util: EC2MetadataUtils: ERROR spring: + geocoder: ${GEOCODER_API_KEY} servlet: multipart: max-file-size: 5MB