diff --git a/api/src/main/java/com/walking/api/converter/TrafficDetailConverter.java b/api/src/main/java/com/walking/api/converter/TrafficDetailConverter.java index 4196f561..1ff74cc5 100644 --- a/api/src/main/java/com/walking/api/converter/TrafficDetailConverter.java +++ b/api/src/main/java/com/walking/api/converter/TrafficDetailConverter.java @@ -1,9 +1,13 @@ package com.walking.api.converter; import com.walking.api.service.dto.PredictedData; +import com.walking.api.web.dto.response.detail.FavoriteTrafficDetail; import com.walking.api.web.dto.response.detail.PointDetail; import com.walking.api.web.dto.response.detail.TrafficDetail; import com.walking.data.entity.traffic.TrafficEntity; +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; public final class TrafficDetailConverter { @@ -15,21 +19,59 @@ private TrafficDetailConverter() {} * @param predictedData 사이클 정보 와 현재 색상 및 잔여시간을 예측한 데이터 * @return 예측 값을 바탕으로 만든 TrafficDetail */ - public static TrafficDetail execute(PredictedData predictedData) { + public static TrafficDetail execute( + PredictedData predictedData, Optional favoriteTrafficDetail) { TrafficEntity trafficEntity = predictedData.getTraffic(); + boolean isFavorite = false; + String viewName = trafficEntity.getName(); + + if (favoriteTrafficDetail.isPresent() + && favoriteTrafficDetail.get().getId().equals(trafficEntity.getId())) { + isFavorite = true; + viewName = favoriteTrafficDetail.get().getName(); + } return TrafficDetail.builder() .id(trafficEntity.getId()) - .color(predictedData.getCurrentColor().toString()) - .timeLeft(predictedData.getCurrentTimeLeft()) + .color(predictedData.getCurrentColorDescription()) + .timeLeft(predictedData.getCurrentTimeLeft().orElse(null)) .point( PointDetail.builder().lng(trafficEntity.getLng()).lat(trafficEntity.getLat()).build()) - .redCycle(predictedData.getRedCycle()) - .greenCycle(predictedData.getGreenCycle()) + .redCycle(predictedData.getRedCycle().orElse(null)) + .greenCycle(predictedData.getGreenCycle().orElse(null)) .detail(TrafficDetailInfoConverter.execute(trafficEntity)) - .isFavorite(false) - .viewName(trafficEntity.getName()) + .isFavorite(isFavorite) + .viewName(viewName) .build(); } + + /** + * PredictedData를 기반으로 TrafficDetail의 List를 생성합니다. + * + * @param predictedData 사이클 정보 와 현재 색상 및 잔여시간을 예측한 데이터 리스트 + * @return 예측 값을 바탕으로 만든 TrafficDetail의 List + */ + public static List execute(List predictedData) { + + return predictedData.stream() + .map( + predictedDatum -> + TrafficDetail.builder() + .id(predictedDatum.getTraffic().getId()) + .color(predictedDatum.getCurrentColorDescription()) + .timeLeft(predictedDatum.getCurrentTimeLeft().orElse(null)) + .point( + PointDetail.builder() + .lng(predictedDatum.getTraffic().getLng()) + .lat(predictedDatum.getTraffic().getLat()) + .build()) + .redCycle(predictedDatum.getRedCycle().orElse(null)) + .greenCycle(predictedDatum.getGreenCycle().orElse(null)) + .detail(TrafficDetailInfoConverter.execute(predictedDatum.getTraffic())) + .isFavorite(false) + .viewName(predictedDatum.getTraffic().getName()) + .build()) + .collect(Collectors.toList()); + } } diff --git a/api/src/main/java/com/walking/api/domain/traffic/dto/AddFavoriteTrafficUseCaseRequest.java b/api/src/main/java/com/walking/api/domain/traffic/dto/AddFavoriteTrafficUseCaseRequest.java new file mode 100644 index 00000000..8cd9d2d5 --- /dev/null +++ b/api/src/main/java/com/walking/api/domain/traffic/dto/AddFavoriteTrafficUseCaseRequest.java @@ -0,0 +1,21 @@ +package com.walking.api.domain.traffic.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class AddFavoriteTrafficUseCaseRequest { + + private Long memberId; + private Long trafficId; + private String trafficAlias; +} diff --git a/api/src/main/java/com/walking/api/domain/traffic/dto/BrowseFavoriteTrafficsUseCaseRequest.java b/api/src/main/java/com/walking/api/domain/traffic/dto/BrowseFavoriteTrafficsUseCaseRequest.java new file mode 100644 index 00000000..3e896755 --- /dev/null +++ b/api/src/main/java/com/walking/api/domain/traffic/dto/BrowseFavoriteTrafficsUseCaseRequest.java @@ -0,0 +1,19 @@ +package com.walking.api.domain.traffic.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +@Builder(toBuilder = true) +public class BrowseFavoriteTrafficsUseCaseRequest { + + private Long memberId; +} diff --git a/api/src/main/java/com/walking/api/domain/traffic/dto/DeleteFavoriteTrafficUseCaseRequest.java b/api/src/main/java/com/walking/api/domain/traffic/dto/DeleteFavoriteTrafficUseCaseRequest.java new file mode 100644 index 00000000..a8606294 --- /dev/null +++ b/api/src/main/java/com/walking/api/domain/traffic/dto/DeleteFavoriteTrafficUseCaseRequest.java @@ -0,0 +1,20 @@ +package com.walking.api.domain.traffic.dto; + +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor +@NoArgsConstructor +@Builder +public class DeleteFavoriteTrafficUseCaseRequest { + + private Long favoriteTrafficId; + private Long memberId; +} diff --git a/api/src/main/java/com/walking/api/domain/traffic/dto/UpdateFavoriteTrafficUseCaseRequest.java b/api/src/main/java/com/walking/api/domain/traffic/dto/UpdateFavoriteTrafficUseCaseRequest.java new file mode 100644 index 00000000..1a862d43 --- /dev/null +++ b/api/src/main/java/com/walking/api/domain/traffic/dto/UpdateFavoriteTrafficUseCaseRequest.java @@ -0,0 +1,22 @@ +package com.walking.api.domain.traffic.dto; + +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@EqualsAndHashCode +@AllArgsConstructor(access = AccessLevel.PRIVATE) +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +public class UpdateFavoriteTrafficUseCaseRequest { + + private Long memberId; + private Long favoriteTrafficId; + private String trafficAlias; +} diff --git a/api/src/main/java/com/walking/api/domain/traffic/usecase/AddFavoriteTrafficUseCase.java b/api/src/main/java/com/walking/api/domain/traffic/usecase/AddFavoriteTrafficUseCase.java new file mode 100644 index 00000000..df0024dd --- /dev/null +++ b/api/src/main/java/com/walking/api/domain/traffic/usecase/AddFavoriteTrafficUseCase.java @@ -0,0 +1,32 @@ +package com.walking.api.domain.traffic.usecase; + +import com.walking.api.domain.traffic.dto.AddFavoriteTrafficUseCaseRequest; +import com.walking.api.repository.traffic.TrafficFavoritesRepository; +import com.walking.data.entity.member.MemberEntity; +import com.walking.data.entity.member.TrafficFavoritesEntity; +import com.walking.data.entity.traffic.TrafficEntity; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class AddFavoriteTrafficUseCase { + + private final TrafficFavoritesRepository trafficFavoritesRepository; + + @Transactional + public boolean execute(AddFavoriteTrafficUseCaseRequest request) { + TrafficFavoritesEntity entity = + TrafficFavoritesEntity.builder() + .memberFk(MemberEntity.builder().id(request.getMemberId()).build()) + .trafficFk(TrafficEntity.builder().id(request.getTrafficId()).build()) + .alias(request.getTrafficAlias()) + .build(); + + trafficFavoritesRepository.save(entity); + return true; + } +} diff --git a/api/src/main/java/com/walking/api/domain/traffic/usecase/BrowseFavoriteTrafficsUseCase.java b/api/src/main/java/com/walking/api/domain/traffic/usecase/BrowseFavoriteTrafficsUseCase.java new file mode 100644 index 00000000..f86830f5 --- /dev/null +++ b/api/src/main/java/com/walking/api/domain/traffic/usecase/BrowseFavoriteTrafficsUseCase.java @@ -0,0 +1,51 @@ +package com.walking.api.domain.traffic.usecase; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.walking.api.domain.traffic.dto.BrowseFavoriteTrafficsUseCaseRequest; +import com.walking.api.repository.traffic.TrafficFavoritesRepository; +import com.walking.api.web.dto.response.BrowseFavoriteTrafficsResponse; +import com.walking.api.web.dto.response.detail.FavoriteTrafficDetail; +import com.walking.api.web.dto.response.detail.PointDetail; +import com.walking.api.web.dto.response.detail.TrafficDetailInfo; +import com.walking.data.entity.member.MemberEntity; +import com.walking.data.entity.member.TrafficFavoritesEntity; +import java.util.ArrayList; +import java.util.List; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.locationtech.jts.geom.Point; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class BrowseFavoriteTrafficsUseCase { + + private final TrafficFavoritesRepository trafficFavoritesRepository; + private final ObjectMapper objectMapper; + + @Transactional + public BrowseFavoriteTrafficsResponse execute(BrowseFavoriteTrafficsUseCaseRequest request) { + List trafficFavorites = + trafficFavoritesRepository.findByMemberFk( + MemberEntity.builder().id(request.getMemberId()).build()); + + List details = new ArrayList<>(); + for (TrafficFavoritesEntity entity : trafficFavorites) { + TrafficDetailInfo detailInfo = + objectMapper.convertValue(entity.getTrafficFk().getDetail(), TrafficDetailInfo.class); + Point point = entity.getTrafficFk().getPoint(); + details.add( + FavoriteTrafficDetail.builder() + .id(entity.getId()) + .detail(detailInfo) + .name(entity.getAlias()) + .point(PointDetail.builder().lat(point.getY()).lng(point.getX()).build()) + .createdAt(entity.getCreatedAt()) + .build()); + } + + return BrowseFavoriteTrafficsResponse.builder().traffics(details).build(); + } +} diff --git a/api/src/main/java/com/walking/api/domain/traffic/usecase/DeleteFavoriteTrafficUseCase.java b/api/src/main/java/com/walking/api/domain/traffic/usecase/DeleteFavoriteTrafficUseCase.java new file mode 100644 index 00000000..8caf3dad --- /dev/null +++ b/api/src/main/java/com/walking/api/domain/traffic/usecase/DeleteFavoriteTrafficUseCase.java @@ -0,0 +1,29 @@ +package com.walking.api.domain.traffic.usecase; + +import com.walking.api.domain.traffic.dto.DeleteFavoriteTrafficUseCaseRequest; +import com.walking.api.repository.traffic.TrafficFavoritesRepository; +import com.walking.data.entity.member.MemberEntity; +import com.walking.data.entity.member.TrafficFavoritesEntity; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class DeleteFavoriteTrafficUseCase { + private final TrafficFavoritesRepository trafficFavoritesRepository; + + @Transactional + public boolean execute(DeleteFavoriteTrafficUseCaseRequest request) { + TrafficFavoritesEntity favoriteTraffic = + trafficFavoritesRepository + .findByIdAndMemberFkAndDeletedFalse( + request.getFavoriteTrafficId(), + MemberEntity.builder().id(request.getMemberId()).build()) + .orElseThrow(() -> new IllegalArgumentException("해당 즐겨찾기 정보가 존재하지 않습니다.")); + trafficFavoritesRepository.delete(favoriteTraffic); + return true; + } +} diff --git a/api/src/main/java/com/walking/api/domain/traffic/usecase/UpdateFavoriteTrafficUseCase.java b/api/src/main/java/com/walking/api/domain/traffic/usecase/UpdateFavoriteTrafficUseCase.java new file mode 100644 index 00000000..ad2e9a98 --- /dev/null +++ b/api/src/main/java/com/walking/api/domain/traffic/usecase/UpdateFavoriteTrafficUseCase.java @@ -0,0 +1,29 @@ +package com.walking.api.domain.traffic.usecase; + +import com.walking.api.domain.traffic.dto.UpdateFavoriteTrafficUseCaseRequest; +import com.walking.api.repository.traffic.TrafficFavoritesRepository; +import com.walking.data.entity.member.TrafficFavoritesEntity; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Slf4j +@Service +@RequiredArgsConstructor +public class UpdateFavoriteTrafficUseCase { + + private final TrafficFavoritesRepository trafficFavoritesRepository; + + @Transactional + public boolean execute(UpdateFavoriteTrafficUseCaseRequest request) { + TrafficFavoritesEntity favoriteTraffic = + trafficFavoritesRepository + .findByIdAndDeletedFalse(request.getFavoriteTrafficId()) + .orElseThrow(() -> new RuntimeException("TrafficFavoritesEntity not found")); + + TrafficFavoritesEntity updatedAlias = favoriteTraffic.updateAlias(request.getTrafficAlias()); + trafficFavoritesRepository.save(updatedAlias); + return true; + } +} diff --git a/api/src/main/java/com/walking/api/repository/traffic/TrafficFavoritesRepository.java b/api/src/main/java/com/walking/api/repository/traffic/TrafficFavoritesRepository.java index 20ca2a7d..25a9baec 100644 --- a/api/src/main/java/com/walking/api/repository/traffic/TrafficFavoritesRepository.java +++ b/api/src/main/java/com/walking/api/repository/traffic/TrafficFavoritesRepository.java @@ -3,6 +3,7 @@ import com.walking.data.entity.member.MemberEntity; import com.walking.data.entity.member.TrafficFavoritesEntity; import java.util.List; +import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; @@ -10,4 +11,9 @@ public interface TrafficFavoritesRepository extends JpaRepository { List findByMemberFk(MemberEntity memberFk); + + Optional findByIdAndDeletedFalse(Long id); + + Optional findByIdAndMemberFkAndDeletedFalse( + Long id, MemberEntity memberFk); } diff --git a/api/src/main/java/com/walking/api/repository/traffic/TrafficRepository.java b/api/src/main/java/com/walking/api/repository/traffic/TrafficRepository.java index 363ab498..a475459f 100644 --- a/api/src/main/java/com/walking/api/repository/traffic/TrafficRepository.java +++ b/api/src/main/java/com/walking/api/repository/traffic/TrafficRepository.java @@ -13,6 +13,7 @@ public interface TrafficRepository extends JpaRepository { @Query("SELECT t FROM TrafficEntity t where t.id IN :ids") List findByIds(@Param("ids") List ids); + // 주변 1km의 Polygon을 만들어 인덱스를 타도록 @Query( value = @@ -33,4 +34,37 @@ public interface TrafficRepository extends JpaRepository { nativeQuery = true) List findClosetTrafficByLocation( @Param("longitude") Double longitude, @Param("latitude") Double latitude); + + @Query( + value = + "SELECT * FROM traffic " + + "WHERE ST_Equals(point_value, " + + "ST_PointFromText(CONCAT('POINT(', :lat, ' ', :lng, ')'), 4326))", + nativeQuery = true) + List findByLocation(@Param("lat") Double lat, @Param("lng") Double lng); + + @Query( + "SELECT t FROM TrafficEntity t " + + "WHERE FUNCTION('ST_Distance_Sphere', t.point, " + + "FUNCTION('ST_PointFromText', CONCAT('POINT(', :lat, ' ', :lng, ')'), 4326)) < :distance") + List findByLocationAndDistance( + @Param("lat") Float lat, @Param("lng") Float lng, @Param("distance") Integer distance); + + @Query( + value = + "SELECT * FROM traffic " + + "WHERE ST_Contains(" + + " ST_SRID(" + + " ST_MakeEnvelope(" + + " POINT(:blLng, :blLat), " + + " POINT(:trLng, :trLat)" + + " ), 4326" + + " ), point_value" + + ")", + nativeQuery = true) + List findTrafficWithinBounds( + @Param("blLng") float blLng, + @Param("blLat") float blLat, + @Param("trLng") float trLng, + @Param("trLat") float trLat); } diff --git a/api/src/main/java/com/walking/api/service/dto/PredictedData.java b/api/src/main/java/com/walking/api/service/dto/PredictedData.java index e3257ee6..9243829c 100644 --- a/api/src/main/java/com/walking/api/service/dto/PredictedData.java +++ b/api/src/main/java/com/walking/api/service/dto/PredictedData.java @@ -32,7 +32,7 @@ public PredictedData(TrafficEntity traffic) { * @return redCycle 이 존재하면 true */ public boolean isPredictedRedCycle() { - return Objects.nonNull(getRedCycle()); + return getRedCycle().isPresent(); } /** @@ -41,7 +41,7 @@ public boolean isPredictedRedCycle() { * @return greenCycle 이 존재하면 true */ public boolean isPredictedGreenCycle() { - return Objects.nonNull(getGreenCycle()); + return getGreenCycle().isPresent(); } /** @@ -104,4 +104,27 @@ public void updateCurrentColor(TrafficColor color) { public void updateCurrentTimeLeft(Float timeLeft) { this.currentTimeLeft = timeLeft; } + + public Optional getRedCycle() { + return Optional.ofNullable(redCycle); + } + + public Optional getGreenCycle() { + return Optional.ofNullable(greenCycle); + } + + public Optional getCurrentColor() { + return Optional.ofNullable(currentColor); + } + + public Optional getCurrentTimeLeft() { + return Optional.ofNullable(currentTimeLeft); + } + + public String getCurrentColorDescription() { + if (getCurrentColor().isPresent()) { + return getCurrentColor().get().toString(); + } + return ""; + } } diff --git a/api/src/main/java/com/walking/api/service/traffic/ReadTrafficService.java b/api/src/main/java/com/walking/api/service/traffic/ReadTrafficService.java index 2bb8a842..84fb3df1 100644 --- a/api/src/main/java/com/walking/api/service/traffic/ReadTrafficService.java +++ b/api/src/main/java/com/walking/api/service/traffic/ReadTrafficService.java @@ -5,6 +5,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; @Service @RequiredArgsConstructor @@ -12,11 +13,24 @@ public class ReadTrafficService { private final TrafficRepository trafficRepository; + @Transactional(readOnly = true) public TrafficEntity executeById(Long trafficId) { return trafficRepository.findById(trafficId).orElseThrow(); } + @Transactional(readOnly = true) public List executeByIds(List trafficIds) { return trafficRepository.findByIds(trafficIds); } + + @Transactional(readOnly = true) + public List executeByLocationAndDistance(Float lat, Float lng, Integer distance) { + return trafficRepository.findByLocationAndDistance(lat, lng, distance); + } + + @Transactional(readOnly = true) + public List executeWithinBounds( + Float blLng, Float blLat, Float trLng, Float trLat) { + return trafficRepository.findTrafficWithinBounds(blLng, blLat, trLng, trLat); + } } diff --git a/api/src/main/java/com/walking/api/web/controller/traffic/TrafficController.java b/api/src/main/java/com/walking/api/web/controller/traffic/TrafficController.java index 922d46a9..e177569e 100644 --- a/api/src/main/java/com/walking/api/web/controller/traffic/TrafficController.java +++ b/api/src/main/java/com/walking/api/web/controller/traffic/TrafficController.java @@ -1,13 +1,24 @@ package com.walking.api.web.controller.traffic; import com.walking.api.converter.TrafficDetailConverter; +import com.walking.api.domain.traffic.dto.AddFavoriteTrafficUseCaseRequest; +import com.walking.api.domain.traffic.dto.BrowseFavoriteTrafficsUseCaseRequest; +import com.walking.api.domain.traffic.dto.DeleteFavoriteTrafficUseCaseRequest; +import com.walking.api.domain.traffic.dto.UpdateFavoriteTrafficUseCaseRequest; +import com.walking.api.domain.traffic.usecase.AddFavoriteTrafficUseCase; +import com.walking.api.domain.traffic.usecase.BrowseFavoriteTrafficsUseCase; +import com.walking.api.domain.traffic.usecase.DeleteFavoriteTrafficUseCase; +import com.walking.api.domain.traffic.usecase.UpdateFavoriteTrafficUseCase; import com.walking.api.security.authentication.token.TokenUserDetails; +import com.walking.api.security.authentication.token.TokenUserDetailsService; +import com.walking.api.security.filter.token.AccessTokenResolver; import com.walking.api.service.TrafficIntegrationPredictService; import com.walking.api.service.dto.PredictedData; import com.walking.api.service.dto.request.IntegrationPredictRequestDto; import com.walking.api.service.dto.response.IntegrationPredictResponseDto; -import com.walking.api.web.dto.request.point.OptionalTrafficPointParam; +import com.walking.api.service.traffic.ReadTrafficService; import com.walking.api.web.dto.request.point.OptionalViewPointParam; +import com.walking.api.web.dto.request.point.ViewPointParam; import com.walking.api.web.dto.request.traffic.FavoriteTrafficBody; import com.walking.api.web.dto.request.traffic.PatchFavoriteTrafficNameBody; import com.walking.api.web.dto.response.BrowseFavoriteTrafficsResponse; @@ -20,17 +31,22 @@ import com.walking.api.web.support.ApiResponse; import com.walking.api.web.support.ApiResponseGenerator; import com.walking.api.web.support.MessageCode; +import com.walking.data.entity.BaseEntity; import java.time.LocalDateTime; +import java.util.ArrayList; import java.util.List; import java.util.Map; import java.util.Objects; +import java.util.Optional; +import java.util.stream.Collectors; +import javax.servlet.http.HttpServletRequest; import javax.validation.Valid; import javax.validation.constraints.Min; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.http.HttpStatus; import org.springframework.security.core.annotation.AuthenticationPrincipal; -import org.springframework.transaction.annotation.Transactional; +import org.springframework.security.core.userdetails.UserDetails; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.DeleteMapping; import org.springframework.web.bind.annotation.GetMapping; @@ -49,6 +65,13 @@ public class TrafficController { private final TrafficIntegrationPredictService integrationPredictService; + private final ReadTrafficService readTrafficService; + private final TokenUserDetailsService tokenUserDetailsService; + + private final AddFavoriteTrafficUseCase addFavoriteTrafficUseCase; + private final BrowseFavoriteTrafficsUseCase browseFavoriteTrafficsUseCase; + private final DeleteFavoriteTrafficUseCase deleteFavoriteTrafficUseCase; + private final UpdateFavoriteTrafficUseCase updateFavoriteTrafficUseCase; static double TF_BACK_DOOR_LAT = 35.178501; static double TF_BACK_DOOR_LNG = 126.912083; @@ -69,26 +92,37 @@ public class TrafficController { @GetMapping() public ApiResponse> searchTraffics( - @Valid OptionalViewPointParam viewPointParam, - @Valid OptionalTrafficPointParam trafficPointParam) { - if (!Objects.isNull(trafficPointParam) && trafficPointParam.isPresent()) { - // todo implement: trafficPointParam를 이용하여 교차로 정보 조회 - log.info("Search traffics trafficPointParam: {}", trafficPointParam.get()); - SearchTrafficsResponse response = getSearchViewTrafficsResponse(); - return ApiResponseGenerator.success(response, HttpStatus.OK, MessageCode.SUCCESS); - } + @Valid OptionalViewPointParam viewPointParam) { - // todo implement: viewPointParam을 이용하여 교차로 정보 조회 + // viewPointParam을 이용하여 교차로 정보 조회 log.info("Search traffics viewPointParam: {}", viewPointParam.get()); - SearchTrafficsResponse response = getSearchTrafficsResponse(); + ViewPointParam viewPoint = viewPointParam.getViewPointParam(); + float vblLng = viewPoint.getVblLng(); + float vblLat = viewPoint.getVblLat(); + float vtrLng = viewPoint.getVtrLng(); + float vtrLat = viewPoint.getVtrLat(); + + List trafficIds = + readTrafficService.executeWithinBounds(vblLng, vblLat, vtrLng, vtrLat).stream() + .map(BaseEntity::getId) + .collect(Collectors.toList()); + + // trafficDetail 생성 + List predictedData = + new ArrayList<>( + integrationPredictService + .execute(IntegrationPredictRequestDto.builder().trafficIds(trafficIds).build()) + .getPredictedDataMap() + .values()); + + List traffics = TrafficDetailConverter.execute(predictedData); + SearchTrafficsResponse response = SearchTrafficsResponse.builder().traffics(traffics).build(); return ApiResponseGenerator.success(response, HttpStatus.OK, MessageCode.SUCCESS); } @GetMapping("/{trafficId}") - @Transactional public ApiResponse> browseTraffic( - @PathVariable Long trafficId) { - // todo implement + HttpServletRequest request, @PathVariable Long trafficId) { log.info("Traffic browse trafficId: {}", trafficId); IntegrationPredictResponseDto integrationPredictedResponse = integrationPredictService.execute( @@ -96,7 +130,28 @@ public ApiResponse> browseTraffi Map predictedDataMap = integrationPredictedResponse.getPredictedDataMap(); PredictedData predictedData = predictedDataMap.get(trafficId); - TrafficDetail trafficDetail = TrafficDetailConverter.execute(predictedData); + + // 인증 토큰이 헤더에 들어있는지 + String authorization = request.getHeader("Authorization"); + Optional favoriteTrafficDetail = Optional.empty(); + if (Objects.nonNull(authorization)) { + + String token = AccessTokenResolver.resolve(authorization); + UserDetails userDetails = tokenUserDetailsService.loadUserByUsername(token); + Long memberId = Long.valueOf(userDetails.getUsername()); + + BrowseFavoriteTrafficsResponse favoriteTraffics = + browseFavoriteTrafficsUseCase.execute( + BrowseFavoriteTrafficsUseCaseRequest.builder().memberId(memberId).build()); + + favoriteTrafficDetail = + favoriteTraffics.getTraffics().stream() + .filter(traffic -> traffic.getId().equals(trafficId)) + .findFirst(); + } + + TrafficDetail trafficDetail = + TrafficDetailConverter.execute(predictedData, favoriteTrafficDetail); BrowseTrafficsResponse response = BrowseTrafficsResponse.builder().traffic(trafficDetail).build(); return ApiResponseGenerator.success(response, HttpStatus.OK, MessageCode.SUCCESS); @@ -106,9 +161,14 @@ public ApiResponse> browseTraffi public ApiResponse addFavoriteTraffic( @AuthenticationPrincipal TokenUserDetails userDetails, @Valid @RequestBody FavoriteTrafficBody favoriteTrafficBody) { - // todo implement - // Long memberId = Long.valueOf(userDetails.getUsername()); - Long memberId = 999L; + Long memberId = Long.valueOf(userDetails.getUsername()); + boolean response = + addFavoriteTrafficUseCase.execute( + AddFavoriteTrafficUseCaseRequest.builder() + .memberId(memberId) + .trafficId(favoriteTrafficBody.getTrafficId()) + .trafficAlias(favoriteTrafficBody.getTrafficAlias()) + .build()); log.info("Favorite traffic request: {}", favoriteTrafficBody); return ApiResponseGenerator.success(HttpStatus.CREATED, MessageCode.RESOURCE_CREATED); } @@ -116,10 +176,10 @@ public ApiResponse addFavoriteTraffic( @GetMapping("/favorite") public ApiResponse> browseFavoriteTraffics(@AuthenticationPrincipal TokenUserDetails userDetails) { - // todo implement - // Long memberId = Long.valueOf(userDetails.getUsername()); - Long memberId = 999L; - BrowseFavoriteTrafficsResponse response = getBrowseFavoriteTrafficsResponse(); + Long memberId = Long.valueOf(userDetails.getUsername()); + BrowseFavoriteTrafficsResponse response = + browseFavoriteTrafficsUseCase.execute( + BrowseFavoriteTrafficsUseCaseRequest.builder().memberId(memberId).build()); return ApiResponseGenerator.success(response, HttpStatus.OK, MessageCode.SUCCESS); } @@ -128,9 +188,14 @@ public ApiResponse updateFavoriteTraffic( @AuthenticationPrincipal TokenUserDetails userDetails, @Min(1) @PathVariable Long trafficId, @Valid @RequestBody PatchFavoriteTrafficNameBody patchFavoriteTrafficNameBody) { - // todo implement - // Long memberId = Long.valueOf(userDetails.getUsername()); - Long memberId = 999L; + Long memberId = Long.valueOf(userDetails.getUsername()); + boolean response = + updateFavoriteTrafficUseCase.execute( + UpdateFavoriteTrafficUseCaseRequest.builder() + .memberId(memberId) + .favoriteTrafficId(trafficId) + .trafficAlias(patchFavoriteTrafficNameBody.getTrafficAlias()) + .build()); log.info( "Update favorite traffic request: trafficId={}, body={}", trafficId, @@ -141,9 +206,13 @@ public ApiResponse updateFavoriteTraffic( @DeleteMapping("/favorite/{trafficId}") public ApiResponse deleteFavoriteTraffic( @AuthenticationPrincipal TokenUserDetails userDetails, @Min(1) @PathVariable Long trafficId) { - // todo implement - // Long memberId = Long.valueOf(userDetails.getUsername()); - Long memberId = 999L; + Long memberId = Long.valueOf(userDetails.getUsername()); + boolean response = + deleteFavoriteTrafficUseCase.execute( + DeleteFavoriteTrafficUseCaseRequest.builder() + .memberId(memberId) + .favoriteTrafficId(trafficId) + .build()); log.info("Delete favorite traffic request: trafficId={}", trafficId); return ApiResponseGenerator.success(HttpStatus.OK, MessageCode.RESOURCE_DELETED); } diff --git a/api/src/main/java/com/walking/api/web/dto/request/point/ViewPointParam.java b/api/src/main/java/com/walking/api/web/dto/request/point/ViewPointParam.java index feaa99f9..91e0f045 100644 --- a/api/src/main/java/com/walking/api/web/dto/request/point/ViewPointParam.java +++ b/api/src/main/java/com/walking/api/web/dto/request/point/ViewPointParam.java @@ -18,14 +18,14 @@ @Builder public class ViewPointParam { /* 화면 좌측 하단 위도 */ - @LatParam private double vblLat; + @LatParam private float vblLat; /* 화면 좌측 하단 경도 */ - @LngParam private double vblLng; + @LngParam private float vblLng; /* 화면 우측 상단 위도 */ - @LatParam private double vtrLat; + @LatParam private float vtrLat; /* 화면 우측 상단 경도 */ - @LngParam private double vtrLng; + @LngParam private float vtrLng; } diff --git a/api/src/main/java/com/walking/api/web/handler/OptionalViewPointParamArgumentResolver.java b/api/src/main/java/com/walking/api/web/handler/OptionalViewPointParamArgumentResolver.java index 537c684a..829ad6ea 100644 --- a/api/src/main/java/com/walking/api/web/handler/OptionalViewPointParamArgumentResolver.java +++ b/api/src/main/java/com/walking/api/web/handler/OptionalViewPointParamArgumentResolver.java @@ -37,10 +37,10 @@ public Object resolveArgument( return OptionalViewPointParam.builder() .viewPointParam( ViewPointParam.builder() - .vblLat(Double.parseDouble(vblLat)) - .vblLng(Double.parseDouble(vblLng)) - .vtrLat(Double.parseDouble(vtrLat)) - .vtrLng(Double.parseDouble(vtrLng)) + .vblLat(Float.parseFloat(vblLat)) + .vblLng(Float.parseFloat(vblLng)) + .vtrLat(Float.parseFloat(vtrLat)) + .vtrLng(Float.parseFloat(vtrLng)) .build()) .build(); } diff --git a/api/src/test/java/com/walking/api/web/controller/traffic/TrafficControllerTest.java b/api/src/test/java/com/walking/api/web/controller/traffic/TrafficControllerTest.java index 7c0e1cff..4708585f 100644 --- a/api/src/test/java/com/walking/api/web/controller/traffic/TrafficControllerTest.java +++ b/api/src/test/java/com/walking/api/web/controller/traffic/TrafficControllerTest.java @@ -2,6 +2,8 @@ import static com.epages.restdocs.apispec.MockMvcRestDocumentationWrapper.document; import static com.epages.restdocs.apispec.ResourceDocumentation.resource; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.when; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.delete; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.get; import static org.springframework.restdocs.mockmvc.RestDocumentationRequestBuilders.patch; @@ -15,15 +17,26 @@ import com.epages.restdocs.apispec.SimpleType; import com.fasterxml.jackson.databind.ObjectMapper; import com.walking.api.ApiApp; +import com.walking.api.domain.traffic.usecase.AddFavoriteTrafficUseCase; +import com.walking.api.domain.traffic.usecase.BrowseFavoriteTrafficsUseCase; +import com.walking.api.domain.traffic.usecase.DeleteFavoriteTrafficUseCase; +import com.walking.api.domain.traffic.usecase.UpdateFavoriteTrafficUseCase; import com.walking.api.web.controller.description.Description; import com.walking.api.web.dto.request.traffic.FavoriteTrafficBody; import com.walking.api.web.dto.request.traffic.PatchFavoriteTrafficNameBody; +import com.walking.api.web.dto.response.BrowseFavoriteTrafficsResponse; +import com.walking.api.web.dto.response.detail.FavoriteTrafficDetail; +import com.walking.api.web.dto.response.detail.PointDetail; +import com.walking.api.web.dto.response.detail.TrafficDetailInfo; +import java.time.LocalDateTime; +import java.util.List; import org.junit.jupiter.api.DisplayName; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.restdocs.AutoConfigureRestDocs; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.boot.test.mock.mockito.MockBean; import org.springframework.http.MediaType; import org.springframework.restdocs.payload.FieldDescriptor; import org.springframework.restdocs.payload.JsonFieldType; @@ -38,9 +51,23 @@ class TrafficControllerTest { @Autowired private MockMvc mockMvc; @Autowired private ObjectMapper objectMapper; + + @MockBean AddFavoriteTrafficUseCase addFavoriteTrafficUseCase; + @MockBean BrowseFavoriteTrafficsUseCase browseFavoriteTrafficsUseCase; + @MockBean DeleteFavoriteTrafficUseCase deleteFavoriteTrafficUseCase; + @MockBean UpdateFavoriteTrafficUseCase updateFavoriteTrafficUseCase; private static final String TAG = "TrafficControllerTest"; private static final String BASE_URL = "/api/v1/traffics"; + static double TF_BACK_DOOR_LAT = 35.178501; + static double TF_BACK_DOOR_LNG = 126.912083; + static double TF_BACK_THREE_LAT = 35.175841; + static double TF_BACK_THREE_LNG = 126.912491; + static double TF_CHANPUNG_LAT = 35.180332; + static double TF_CHANPUNG_LNG = 126.912123; + static double TF_CUCU_LAT = 35.176495; + static double TF_CUCU_LNG = 126.896888; + @Test @DisplayName("GET 신호등 정보 조회 - 화면 좌표로 조회") void searchTrafficsWithViewPointParam() throws Exception { @@ -49,10 +76,10 @@ void searchTrafficsWithViewPointParam() throws Exception { .perform( get(BASE_URL) .contentType(MediaType.APPLICATION_JSON) - .param("vblLat", "35.175840") - .param("vblLng", "126.912490") - .param("vtrLat", "35.178526") - .param("vtrLng", "124.123457")) + .param("vblLat", "37.595000") + .param("vblLng", "127.07870") + .param("vtrLat", "37.605372") + .param("vtrLng", "127.080309")) .andExpect(status().is2xxSuccessful()) .andDo( document( @@ -310,6 +337,8 @@ void addFavoriteTraffic() throws Exception { FavoriteTrafficBody.builder().trafficId(1L).trafficAlias("alias1").build(); String content = objectMapper.writeValueAsString(body); + when(addFavoriteTrafficUseCase.execute(any())).thenReturn(true); + mockMvc .perform( post(BASE_URL + "/favorite") @@ -334,6 +363,74 @@ void addFavoriteTraffic() throws Exception { @Test @DisplayName("GET /favorite 즐겨찾기 신호등 조회") void browseFavoriteTraffics() throws Exception { + + when(browseFavoriteTrafficsUseCase.execute(any())) + .thenReturn( + BrowseFavoriteTrafficsResponse.builder() + .traffics( + List.of( + FavoriteTrafficDetail.builder() + .id(1L) + .detail( + TrafficDetailInfo.builder() + .trafficId(1L) + .apiSource("seoul") + .direction("nt") + .build()) + .name("후문") + .point( + PointDetail.builder() + .lat(TF_BACK_DOOR_LAT) + .lng(TF_BACK_DOOR_LNG) + .build()) + .createdAt(LocalDateTime.now()) + .build(), + FavoriteTrafficDetail.builder() + .id(2L) + .detail( + TrafficDetailInfo.builder() + .trafficId(2L) + .apiSource("seoul") + .direction("wt") + .build()) + .name("후문3거리") + .point( + PointDetail.builder() + .lat(TF_BACK_THREE_LAT) + .lng(TF_BACK_THREE_LNG) + .build()) + .createdAt(LocalDateTime.now()) + .build(), + FavoriteTrafficDetail.builder() + .id(3L) + .detail( + TrafficDetailInfo.builder() + .trafficId(3L) + .apiSource("seoul") + .direction("sw") + .build()) + .name("창평") + .point( + PointDetail.builder() + .lat(TF_CHANPUNG_LAT) + .lng(TF_CHANPUNG_LNG) + .build()) + .createdAt(LocalDateTime.now()) + .build(), + FavoriteTrafficDetail.builder() + .id(4L) + .detail( + TrafficDetailInfo.builder() + .trafficId(4L) + .apiSource("seoul") + .direction("nt") + .build()) + .name("쿠쿠") + .point(PointDetail.builder().lat(TF_CUCU_LAT).lng(TF_CUCU_LNG).build()) + .createdAt(LocalDateTime.now()) + .build())) + .build()); + mockMvc .perform( get(BASE_URL + "/favorite") @@ -400,6 +497,8 @@ void updateFavoriteTraffic() throws Exception { PatchFavoriteTrafficNameBody.builder().trafficAlias("alias2").build(); String content = objectMapper.writeValueAsString(body); + when(updateFavoriteTrafficUseCase.execute(any())).thenReturn(true); + mockMvc .perform( patch(BASE_URL + "/favorite/{favoriteId}", 1) @@ -429,6 +528,9 @@ void updateFavoriteTraffic() throws Exception { @Test @DisplayName("DELETE /favorite/{favoriteId} 즐겨찾기 신호등 삭제") void deleteFavoriteTraffic() throws Exception { + + when(deleteFavoriteTrafficUseCase.execute(any())).thenReturn(true); + mockMvc .perform( delete(BASE_URL + "/favorite/{favoriteId}", 1) diff --git a/data/src/main/java/com/walking/data/entity/member/TrafficFavoritesEntity.java b/data/src/main/java/com/walking/data/entity/member/TrafficFavoritesEntity.java index 63b22dd8..4b37620b 100644 --- a/data/src/main/java/com/walking/data/entity/member/TrafficFavoritesEntity.java +++ b/data/src/main/java/com/walking/data/entity/member/TrafficFavoritesEntity.java @@ -29,4 +29,10 @@ public class TrafficFavoritesEntity extends BaseEntity { @Column(nullable = false, length = 50) private String alias; + + // todo delete + public TrafficFavoritesEntity updateAlias(String alias) { + this.alias = alias; + return this; + } }