From 532d11ca7f7d17cb4e2f2fa4d0b30b724d4ea9d2 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 12 Aug 2025 12:29:19 +0900 Subject: [PATCH 01/23] =?UTF-8?q?[test]=20=EA=B2=BD=EC=82=AC=EB=A1=9C?= =?UTF-8?q?=EA=B0=80=20=EC=9E=88=EC=9D=84=20=EB=96=84=EC=9D=98=20=EC=9B=A8?= =?UTF-8?q?=EC=9D=B4=EB=B8=94=EC=A1=B4=20=EC=B6=94=EC=B2=9C=20=EA=B2=BD?= =?UTF-8?q?=EB=A1=9C=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../direction/service/WalkingServiceTest.java | 105 ++++++++++++++++++ 1 file changed, 105 insertions(+) create mode 100644 src/test/java/com/wayble/server/direction/service/WalkingServiceTest.java diff --git a/src/test/java/com/wayble/server/direction/service/WalkingServiceTest.java b/src/test/java/com/wayble/server/direction/service/WalkingServiceTest.java new file mode 100644 index 00000000..db06bbbc --- /dev/null +++ b/src/test/java/com/wayble/server/direction/service/WalkingServiceTest.java @@ -0,0 +1,105 @@ +package com.wayble.server.direction.service; + +import com.wayble.server.direction.dto.response.WayblePathResponse; +import com.wayble.server.direction.entity.Edge; +import com.wayble.server.direction.entity.Node; +import com.wayble.server.direction.entity.type.Type; +import com.wayble.server.direction.init.GraphInit; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.InjectMocks; +import org.mockito.Mock; +import org.mockito.junit.jupiter.MockitoExtension; + +import java.util.List; +import java.util.Map; + +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.Mockito.when; +import static org.junit.jupiter.api.Assertions.*; + +@ExtendWith(MockitoExtension.class) +public class WalkingServiceTest { + + @Mock + private GraphInit graphInit; + + @Mock + WaybleDijkstraService waybleDijkstraService; + + @InjectMocks + private WalkingService walkingService; + + @BeforeEach + void setUp() { + Map nodeMap = Map.of( + 1L, new Node(1L, 37.1, 127.1), + 2L, new Node(2L, 37.2, 127.1), + 3L, new Node(3L, 37.3, 127.1), + 4L, new Node(4L, 37.2, 127.2), + 5L, new Node(5L, 37.3, 127.2) + ); + + Map> adjacencyList = Map.of( + 1L, List.of(new Edge(1, 2, 100, List.of())), + 2L, List.of(new Edge(2, 3, 100, List.of()), new Edge(2, 4, 120, List.of())), + 3L, List.of(new Edge(3, 5, 90, List.of())), + 4L, List.of(new Edge(4, 5, 120, List.of())) + ); + + Map markerMap = Map.of( + 2L, Type.RAMP, + 3L, Type.NONE, + 4L, Type.ELEVATOR + ); + + List points = List.of( + new WayblePathResponse.WayblePoint(37.1, 127.1, Type.NONE), + new WayblePathResponse.WayblePoint(37.2, 127.1, Type.RAMP), + new WayblePathResponse.WayblePoint(37.2, 127.2, Type.ELEVATOR), + new WayblePathResponse.WayblePoint(37.3, 127.2, Type.NONE) + ); + + List polyline = List.of( + new double[]{37.1, 127.1}, + new double[]{37.2, 127.1}, + new double[]{37.2, 127.2}, + new double[]{37.3, 127.2} + ); + + when(graphInit.getNodeMap()).thenReturn(nodeMap); + // when(graphInit.getMarkerMap()).thenReturn(markerMap); + // when(graphInit.getGraph()).thenReturn(adjacencyList); + when(waybleDijkstraService.createWayblePath(anyLong(), anyLong())) + .thenReturn(WayblePathResponse.of(340, 300, points, polyline)); + } + + @Test + @DisplayName("경사로가 있을 때의 웨이블 추천 경로") + void rampWayblePathTest() { + // given + double startLat = 37.1; + double startLon = 127.1; + double endLat = 37.3; + double endLon = 127.2; + + // when + WayblePathResponse response = walkingService.findWayblePath( + startLat, startLon, endLat, endLon + ); + + // then + assertNotNull(response); + + assertEquals( + List.of( + new WayblePathResponse.WayblePoint(37.1, 127.1, Type.NONE), + new WayblePathResponse.WayblePoint(37.2, 127.1, Type.RAMP), + new WayblePathResponse.WayblePoint(37.2, 127.2, Type.ELEVATOR), + new WayblePathResponse.WayblePoint(37.3, 127.2, Type.NONE) + ), response.points() + ); + } +} From 54dd3eb8529c9a9e157272b5076712509f52e07e Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 13:12:59 +0900 Subject: [PATCH 02/23] =?UTF-8?q?[feat]=20FacilityDocument=20=EC=97=94?= =?UTF-8?q?=ED=8B=B0=ED=8B=B0,=20dto=20=EC=A0=95=EC=9D=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dto/common/FacilityResponseDto.java | 4 +-- .../facility/WaybleFacilityConditionDto.java | 21 +++++++++++++++ .../facility/WaybleFacilityRegisterDto.java | 23 ++++++++++++++++ .../facility/WaybleFacilityResponseDto.java | 12 +++++++++ .../server/explore/entity/FacilityType.java | 6 +++++ .../entity/WaybleFacilityDocument.java | 27 +++++++++++++++++++ 6 files changed, 91 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityConditionDto.java create mode 100644 src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityRegisterDto.java create mode 100644 src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java create mode 100644 src/main/java/com/wayble/server/explore/entity/FacilityType.java create mode 100644 src/main/java/com/wayble/server/explore/entity/WaybleFacilityDocument.java diff --git a/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java b/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java index 81e91253..97ccee7b 100644 --- a/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java +++ b/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java @@ -1,6 +1,6 @@ package com.wayble.server.explore.dto.common; -import com.wayble.server.explore.entity.EsWaybleZoneFacility; +import com.wayble.server.explore.entity.EsWaybleFacility; import lombok.Builder; @Builder @@ -12,7 +12,7 @@ public record FacilityResponseDto( Boolean hasDisabledToilet, String floorInfo ) { - public static FacilityResponseDto from(EsWaybleZoneFacility facility) { + public static FacilityResponseDto from(EsWaybleFacility facility) { if (facility == null) { return null; } diff --git a/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityConditionDto.java b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityConditionDto.java new file mode 100644 index 00000000..5c774574 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityConditionDto.java @@ -0,0 +1,21 @@ +package com.wayble.server.explore.dto.facility; + +import com.wayble.server.explore.entity.FacilityType; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; + +public record WaybleFacilityConditionDto( + @DecimalMin(value = "-90.0", message = "위도는 -90.0 이상이어야 합니다.") + @DecimalMax(value = "90.0", message = "위도는 90.0 이하여야 합니다.") + @NotNull(message = "위도 입력은 필수입니다.") + Double latitude, + + @DecimalMin(value = "-180.0", message = "경도는 -180.0 이상이어야 합니다.") + @DecimalMax(value = "180.0", message = "경도는 180.0 이하여야 합니다.") + @NotNull(message = "경도 입력은 필수입니다.") + Double longitude, + + FacilityType facilityType +) { +} diff --git a/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityRegisterDto.java b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityRegisterDto.java new file mode 100644 index 00000000..311c2fc0 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityRegisterDto.java @@ -0,0 +1,23 @@ +package com.wayble.server.explore.dto.facility; + +import com.wayble.server.explore.entity.FacilityType; +import jakarta.validation.constraints.DecimalMax; +import jakarta.validation.constraints.DecimalMin; +import jakarta.validation.constraints.NotNull; +import lombok.Builder; + +@Builder +public record WaybleFacilityRegisterDto ( + @DecimalMin(value = "-90.0", message = "위도는 -90.0 이상이어야 합니다.") + @DecimalMax(value = "90.0", message = "위도는 90.0 이하여야 합니다.") + @NotNull(message = "위도 입력은 필수입니다.") + Double latitude, + + @DecimalMin(value = "-180.0", message = "경도는 -180.0 이상이어야 합니다.") + @DecimalMax(value = "180.0", message = "경도는 180.0 이하여야 합니다.") + @NotNull(message = "경도 입력은 필수입니다.") + Double longitude, + + FacilityType facilityType +){ +} diff --git a/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java new file mode 100644 index 00000000..643d8520 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java @@ -0,0 +1,12 @@ +package com.wayble.server.explore.dto.facility; + +import com.wayble.server.explore.entity.FacilityType; + +public record WaybleFacilityResponseDto( + Double latitude, + + Double longitude, + + FacilityType facilityType +) { +} diff --git a/src/main/java/com/wayble/server/explore/entity/FacilityType.java b/src/main/java/com/wayble/server/explore/entity/FacilityType.java new file mode 100644 index 00000000..c3ab5b30 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/entity/FacilityType.java @@ -0,0 +1,6 @@ +package com.wayble.server.explore.entity; + +public enum FacilityType { + ELEVATOR, + RAMP +} diff --git a/src/main/java/com/wayble/server/explore/entity/WaybleFacilityDocument.java b/src/main/java/com/wayble/server/explore/entity/WaybleFacilityDocument.java new file mode 100644 index 00000000..56dcf3b5 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/entity/WaybleFacilityDocument.java @@ -0,0 +1,27 @@ +package com.wayble.server.explore.entity; + +import com.wayble.server.direction.entity.transportation.Facility; +import com.wayble.server.explore.dto.facility.WaybleFacilityRegisterDto; +import lombok.*; +import org.springframework.data.annotation.Id; +import org.springframework.data.elasticsearch.annotations.Document; +import org.springframework.data.elasticsearch.annotations.Field; +import org.springframework.data.elasticsearch.annotations.GeoPointField; +import org.springframework.data.elasticsearch.core.geo.GeoPoint; + +@ToString +@Builder +@Getter +@AllArgsConstructor +@NoArgsConstructor +@Document(indexName = "wayble_facility_document", createIndex = true) +public class WaybleFacilityDocument { + @Id + @Field(name = "id") + private String id; + + @GeoPointField + private GeoPoint location; + + private FacilityType facilityType; +} From fd99f0f06f3b4472f5f9572b6e05230682864f3f Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 13:23:55 +0900 Subject: [PATCH 03/23] =?UTF-8?q?[feat]=20FacilityDocument=20=EC=A1=B0?= =?UTF-8?q?=ED=9A=8C=20=EC=BB=A8=ED=8A=B8=EB=A1=A4=EB=9F=AC,=20dto=20?= =?UTF-8?q?=EC=A0=95=EC=9D=98=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleFacilitySearchController.java | 30 +++++++++++++++++++ .../dto/common/FacilityResponseDto.java | 4 +-- .../facility/WaybleFacilityResponseDto.java | 11 +++++++ 3 files changed, 43 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/wayble/server/explore/controller/WaybleFacilitySearchController.java diff --git a/src/main/java/com/wayble/server/explore/controller/WaybleFacilitySearchController.java b/src/main/java/com/wayble/server/explore/controller/WaybleFacilitySearchController.java new file mode 100644 index 00000000..077ba1c0 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/controller/WaybleFacilitySearchController.java @@ -0,0 +1,30 @@ +package com.wayble.server.explore.controller; + +import com.wayble.server.common.response.CommonResponse; +import com.wayble.server.explore.dto.facility.WaybleFacilityConditionDto; +import com.wayble.server.explore.dto.facility.WaybleFacilityResponseDto; +import com.wayble.server.explore.service.WaybleFacilityDocumentService; +import jakarta.validation.Valid; +import lombok.RequiredArgsConstructor; +import org.springframework.validation.annotation.Validated; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +@RequiredArgsConstructor +@Validated +@RequestMapping("/api/v1/facilities/search") +public class WaybleFacilitySearchController { + private final WaybleFacilityDocumentService waybleFacilityDocumentService; + + @GetMapping("") + public CommonResponse> findNearbyFacilities( + @Valid @ModelAttribute WaybleFacilityConditionDto conditionDto + ) { + return CommonResponse.success(waybleFacilityDocumentService.findNearbyFacilityDocuments(conditionDto)); + } +} diff --git a/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java b/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java index 97ccee7b..81e91253 100644 --- a/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java +++ b/src/main/java/com/wayble/server/explore/dto/common/FacilityResponseDto.java @@ -1,6 +1,6 @@ package com.wayble.server.explore.dto.common; -import com.wayble.server.explore.entity.EsWaybleFacility; +import com.wayble.server.explore.entity.EsWaybleZoneFacility; import lombok.Builder; @Builder @@ -12,7 +12,7 @@ public record FacilityResponseDto( Boolean hasDisabledToilet, String floorInfo ) { - public static FacilityResponseDto from(EsWaybleFacility facility) { + public static FacilityResponseDto from(EsWaybleZoneFacility facility) { if (facility == null) { return null; } diff --git a/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java index 643d8520..3bbd399d 100644 --- a/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java +++ b/src/main/java/com/wayble/server/explore/dto/facility/WaybleFacilityResponseDto.java @@ -1,7 +1,11 @@ package com.wayble.server.explore.dto.facility; import com.wayble.server.explore.entity.FacilityType; +import com.wayble.server.explore.entity.WaybleFacilityDocument; +import lombok.AccessLevel; +import lombok.Builder; +@Builder(access = AccessLevel.PRIVATE) public record WaybleFacilityResponseDto( Double latitude, @@ -9,4 +13,11 @@ public record WaybleFacilityResponseDto( FacilityType facilityType ) { + public static WaybleFacilityResponseDto from(WaybleFacilityDocument facilityDocument) { + return WaybleFacilityResponseDto.builder() + .latitude(facilityDocument.getLocation().getLat()) + .longitude(facilityDocument.getLocation().getLon()) + .facilityType(facilityDocument.getFacilityType()) + .build(); + } } From a471f5d31435192d5f12631c9c1e06f7cae025ba Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 13:24:04 +0900 Subject: [PATCH 04/23] =?UTF-8?q?[feat]=20FacilityDocument=20repository=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleFacilityQuerySearchRepository.java | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java diff --git a/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java new file mode 100644 index 00000000..d3ba130f --- /dev/null +++ b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java @@ -0,0 +1,101 @@ +package com.wayble.server.explore.repository.facility; + +import co.elastic.clients.elasticsearch._types.GeoLocation; +import co.elastic.clients.elasticsearch._types.SortOptions; +import co.elastic.clients.elasticsearch._types.SortOrder; +import co.elastic.clients.elasticsearch._types.query_dsl.Query; +import com.wayble.server.explore.dto.facility.WaybleFacilityConditionDto; +import com.wayble.server.explore.dto.facility.WaybleFacilityResponseDto; +import com.wayble.server.explore.entity.WaybleFacilityDocument; +import lombok.RequiredArgsConstructor; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.elasticsearch.client.elc.NativeQuery; +import org.springframework.data.elasticsearch.core.ElasticsearchOperations; +import org.springframework.data.elasticsearch.core.SearchHits; +import org.springframework.data.elasticsearch.core.mapping.IndexCoordinates; +import org.springframework.stereotype.Repository; + +import java.util.List; + +@Repository +@RequiredArgsConstructor +public class WaybleFacilityQuerySearchRepository { + + private final ElasticsearchOperations operations; + private static final IndexCoordinates INDEX = IndexCoordinates.of("wayble_facility_document"); + private static final int LIMIT = 50; + + /** + * 위도, 경도, 시설 타입을 바탕으로 WaybleFacilityDocument를 거리순으로 N개 반환 + */ + public List findNearbyFacilitiesByType( + WaybleFacilityConditionDto condition) { + + double radius = 5.0; // 기본 반경 5km + String radiusWithUnit = radius + "km"; + + // 시설 타입에 따른 쿼리 조건 생성 + Query query = Query.of(q -> q + .bool(b -> { + // 시설 타입 조건 추가 + if (condition.facilityType() != null) { + b.must(m -> m + .term(t -> t + .field("facilityType.keyword") + .value(condition.facilityType().name()) + ) + ); + } + + // 위치 기반 필터: 중심 좌표 기준 반경 필터링 + b.filter(f -> f + .geoDistance(gd -> gd + .field("location") + .location(loc -> loc + .latlon(ll -> ll + .lat(condition.latitude()) + .lon(condition.longitude()) + ) + ) + .distance(radiusWithUnit) + ) + ); + + return b; + }) + ); + + // 거리 기준 오름차순 정렬 + SortOptions geoSort = SortOptions.of(s -> s + .geoDistance(gds -> gds + .field("location") + .location(GeoLocation.of(gl -> gl + .latlon(ll -> ll + .lat(condition.latitude()) + .lon(condition.longitude()) + ) + )) + .order(SortOrder.Asc) + ) + ); + + // Elasticsearch 쿼리 구성 + NativeQuery nativeQuery = NativeQuery.builder() + .withQuery(query) + .withSort(geoSort) + .withPageable(PageRequest.of(0, LIMIT)) + .build(); + + // 검색 수행 + SearchHits hits = + operations.search(nativeQuery, WaybleFacilityDocument.class, INDEX); + + // 결과를 Document 리스트로 반환 + return hits.stream() + .map(hit -> { + WaybleFacilityDocument doc = hit.getContent(); + return WaybleFacilityResponseDto.from(doc); + }) + .toList(); + } +} From 72e9bec159bbe4d9f6b481eed54d054c79a89d7e Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 13:24:12 +0900 Subject: [PATCH 05/23] =?UTF-8?q?[feat]=20FacilityDocument=20controller=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=9E=91=EC=84=B1=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleFacilityDocumentService.java | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/main/java/com/wayble/server/explore/service/WaybleFacilityDocumentService.java diff --git a/src/main/java/com/wayble/server/explore/service/WaybleFacilityDocumentService.java b/src/main/java/com/wayble/server/explore/service/WaybleFacilityDocumentService.java new file mode 100644 index 00000000..3d52e258 --- /dev/null +++ b/src/main/java/com/wayble/server/explore/service/WaybleFacilityDocumentService.java @@ -0,0 +1,22 @@ +package com.wayble.server.explore.service; + +import com.wayble.server.explore.dto.facility.WaybleFacilityConditionDto; +import com.wayble.server.explore.dto.facility.WaybleFacilityResponseDto; +import com.wayble.server.explore.repository.facility.WaybleFacilityQuerySearchRepository; +import jakarta.transaction.Transactional; +import lombok.RequiredArgsConstructor; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +@RequiredArgsConstructor +@Transactional +public class WaybleFacilityDocumentService { + + private final WaybleFacilityQuerySearchRepository waybleFacilityQuerySearchRepository; + + public List findNearbyFacilityDocuments(WaybleFacilityConditionDto dto) { + return waybleFacilityQuerySearchRepository.findNearbyFacilitiesByType(dto); + } +} From 2aa9d578b04567cad15704696f6f4b4598cffb12 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 12 Aug 2025 13:40:38 +0900 Subject: [PATCH 06/23] =?UTF-8?q?[fix]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=B6=94=EC=B2=9C=20=EA=B2=BD=EB=A1=9C=20-=20?= =?UTF-8?q?=EA=B2=BD=EC=82=AC=EB=A1=9C=20=EA=B0=80=EC=A4=91=EC=B9=98,=20?= =?UTF-8?q?=EC=A4=91=EB=B3=B5=20=EC=A2=8C=ED=91=9C=20=EC=A0=9C=EA=B1=B0=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/direction/init/GraphInit.java | 8 ++++ .../service/WaybleDijkstraService.java | 38 +++++++++++-------- src/main/resources/wayble_markers.json | 8 ++-- 3 files changed, 34 insertions(+), 20 deletions(-) diff --git a/src/main/java/com/wayble/server/direction/init/GraphInit.java b/src/main/java/com/wayble/server/direction/init/GraphInit.java index bc6fd01b..01ee918b 100644 --- a/src/main/java/com/wayble/server/direction/init/GraphInit.java +++ b/src/main/java/com/wayble/server/direction/init/GraphInit.java @@ -29,6 +29,7 @@ public class GraphInit { private Map> adjacencyList; private Map nodeMap; private Map markerMap; + private final Set rampMakers; @PostConstruct public void init() { @@ -66,7 +67,10 @@ private Map> buildAdjacencyList() { for (Edge edge : edges) { boolean isWaybleMarker = markerMap.containsKey(edge.from()) || markerMap.containsKey(edge.to()); + boolean isRamp = rampMakers.contains(edge.from()) || rampMakers.contains(edge.to()); + double distance = isWaybleMarker ? edge.length() * 0.5 : edge.length(); + if (isRamp) distance *= 5; // 양방향 adjacencyList.computeIfAbsent(edge.from(), k -> new ArrayList<>()) @@ -106,4 +110,8 @@ public Map getMarkerMap() { public Map> getGraph() { return Collections.unmodifiableMap(adjacencyList); } + + public GraphInit(Set rampMakers) { + this.rampMakers = rampMakers; + } } diff --git a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java index 949fccce..360cf5f0 100644 --- a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java +++ b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java @@ -42,7 +42,6 @@ public WayblePathResponse createWayblePath(long start, long end) { private List createPolyLine(List path) { List polyline = new ArrayList<>(); Map> adjacencyList = graphInit.getGraph(); - double[] last = null; for (int i = 0; i < path.size() - 1; i++) { long from = path.get(i); @@ -55,11 +54,8 @@ private List createPolyLine(List path) { // 좌표 중복 제거 (동일 좌표가 연속될 시, 추가 X) if (edge != null && edge.geometry() != null) { - for (double[] coord : edge.geometry()) { - if (last == null || isDifferent(last, coord)) { - polyline.add(coord); - last = coord; - } + for (double[] coords : edge.geometry()) { + deleteDuplicateCoords(polyline, coords); } } else { Node fromNode = graphInit.getNodeMap().get(from); @@ -68,19 +64,29 @@ private List createPolyLine(List path) { double[] fromCoord = new double[]{fromNode.lon(), fromNode.lat()}; double[] toCoord = new double[]{toNode.lon(), toNode.lat()}; - // 중복 확인 후, 중복 X일 때만 추가 - if (last == null || isDifferent(last, fromCoord)) { - polyline.add(fromCoord); - } - if (last == null || isDifferent(last, toCoord)) { - polyline.add(toCoord); - last = toCoord; - } + deleteDuplicateCoords(polyline, fromCoord); + deleteDuplicateCoords(polyline, toCoord); } } return polyline; } + private void deleteDuplicateCoords(List polyline, double[] coords) { + int n = polyline.size(); + + // 연속 중복 좌표 제거 + if (!polyline.isEmpty() && isClose(polyline.get(n - 1), coords)) return; + + // 과거의 좌표로 돌아올 경우, 해당 좌표 제거 + for (int i = n - 2; i >= 0; i--) { + if (isClose(polyline.get(i), coords)) { + polyline.subList(i + 1, n).clear(); + return; + } + } + polyline.add(coords.clone()); + } + private double calculateTime(List path) { double averageSpeed = 1.0; double totalTime = 0.0; @@ -152,7 +158,7 @@ private List dijkstra(long start, long end) { return path; } - private boolean isDifferent(double[] a, double[] b) { - return Math.abs(a[0] - b[0]) > TOLERANCE || Math.abs(a[1] - b[1]) > TOLERANCE; + private boolean isClose(double[] a, double[] b) { + return Math.abs(a[0] - b[0]) <= TOLERANCE && Math.abs(a[1] - b[1]) <= TOLERANCE; } } diff --git a/src/main/resources/wayble_markers.json b/src/main/resources/wayble_markers.json index 37300c8d..e161284c 100644 --- a/src/main/resources/wayble_markers.json +++ b/src/main/resources/wayble_markers.json @@ -123,13 +123,13 @@ "id": 1740825183, "lat": 37.49930939062552, "lon": 127.02269235121697, - "type": "WHEELCHAIR_LIFT" + "type": "RAMP" }, { "id": 1012654209, "lat": 37.47030346142741, "lon": 127.02480372021024, - "type": "WHEELCHAIR_CHARGER" + "type": "RAMP" }, { "id": 4487534788, @@ -159,7 +159,7 @@ "id": 6875654572, "lat": 37.49975423821268, "lon": 127.02250552924745, - "type": "NONE" + "type": "RAMP" }, { "id": 1709551788, @@ -279,7 +279,7 @@ "id": 2747865798, "lat": 37.479060974875445, "lon": 126.99193969295382, - "type": "NONE" + "type": "RAMP" }, { "id": 9869178896, From 65f002a7dda9aaf8ab60f6de1b7b5dd9216d4c91 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 13:42:47 +0900 Subject: [PATCH 07/23] =?UTF-8?q?[feat]=20FacilityTest=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EB=8D=94=EB=AF=B8=20?= =?UTF-8?q?=EB=8D=B0=EC=9D=B4=ED=84=B0=20=EC=83=9D=EC=84=B1=20=EC=99=84?= =?UTF-8?q?=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleFacilityApiIntegrationTest.java | 145 ++++++++++++++++++ 1 file changed, 145 insertions(+) create mode 100644 src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java diff --git a/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java new file mode 100644 index 00000000..0da66cf4 --- /dev/null +++ b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java @@ -0,0 +1,145 @@ +package com.wayble.server.explore; + +import com.fasterxml.jackson.databind.ObjectMapper; +import com.wayble.server.common.config.security.jwt.JwtTokenProvider; +import com.wayble.server.explore.entity.FacilityType; +import com.wayble.server.explore.entity.WaybleFacilityDocument; +import com.wayble.server.explore.entity.WaybleZoneDocument; +import com.wayble.server.explore.repository.facility.WaybleFacilityDocumentRepository; +import com.wayble.server.user.entity.Gender; +import com.wayble.server.user.entity.LoginType; +import com.wayble.server.user.entity.User; +import com.wayble.server.user.entity.UserType; +import com.wayble.server.user.repository.UserRepository; +import org.junit.jupiter.api.AfterAll; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.elasticsearch.core.geo.GeoPoint; +import org.springframework.test.web.servlet.MockMvc; + +import java.time.LocalDate; +import java.util.List; +import java.util.Map; +import java.util.Random; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestInstance(TestInstance.Lifecycle.PER_CLASS) +@AutoConfigureMockMvc +public class WaybleFacilityApiIntegrationTest { + + @Autowired + private MockMvc mockMvc; + + @Autowired + private ObjectMapper objectMapper; + + @Autowired + private JwtTokenProvider jwtTokenProvider; + + @Autowired + private UserRepository userRepository; + + @Autowired + private WaybleFacilityDocumentRepository waybleFacilityDocumentRepository; + + private static final double LATITUDE = 37.5435480; + + private static final double LONGITUDE = 126.9518410; + + private static final double RADIUS = 20.0; + + private static final int SAMPLES = 100; + + private static final String baseUrl = "/api/v1/wayble-zones/facilities"; + + private Long userId; + + private String token; + + @BeforeAll + public void setup() { + User testUser = User.createUserWithDetails( + "testUser", "testUsername", UUID.randomUUID() + "@email", "password", + LocalDate.now(), Gender.MALE, LoginType.KAKAO, UserType.DISABLED + ); + + userRepository.save(testUser); + userId = testUser.getId(); + token = jwtTokenProvider.generateToken(userId, "ROLE_USER"); + + for(int i = 1; i <= SAMPLES; i++) { + Map points = makeRandomPoint(); + + WaybleFacilityDocument rampDocument = WaybleFacilityDocument.builder() + .id(UUID.randomUUID().toString()) + .location(new GeoPoint(points.get("latitude"), points.get("longitude"))) + .facilityType(FacilityType.RAMP) + .build(); + + WaybleFacilityDocument elevatorDocument = WaybleFacilityDocument.builder() + .id(UUID.randomUUID().toString()) + .location(new GeoPoint(points.get("latitude"), points.get("longitude"))) + .facilityType(FacilityType.ELEVATOR) + .build(); + + waybleFacilityDocumentRepository.save(rampDocument); + waybleFacilityDocumentRepository.save(elevatorDocument); + } + } + + @AfterAll + public void teardown() { + waybleFacilityDocumentRepository.deleteAll(); + userRepository.deleteById(userId); + } + + @Test + public void checkDataExists() { + List all = waybleFacilityDocumentRepository.findAll(); + assertThat(all.size()).isGreaterThan(0); + + for (WaybleFacilityDocument doc : all) { + assertThat(doc.getId()).isNotNull(); + assertThat(doc.getLocation()).isNotNull(); + assertThat(doc.getFacilityType()).isNotNull(); + System.out.println(doc); + } + } + + private double haversine(double lat1, double lon1, double lat2, double lon2) { + final int R = 6_371; // 지구 반지름 (km) + double dLat = Math.toRadians(lat2 - lat1); + double dLon = Math.toRadians(lon2 - lon1); + double a = Math.sin(dLat/2) * Math.sin(dLat/2) + + Math.cos(Math.toRadians(lat1)) * Math.cos(Math.toRadians(lat2)) + * Math.sin(dLon/2) * Math.sin(dLon/2); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1-a)); + return R * c; + } + + private Map makeRandomPoint() { + double radiusDeg = RADIUS / 111.0; + + Random rnd = new Random(); + + double u = rnd.nextDouble(); + double v = rnd.nextDouble(); + double w = radiusDeg * Math.sqrt(u); + double t = 2 * Math.PI * v; + + double latOffset = w * Math.cos(t); + double lngOffset = w * Math.sin(t) / Math.cos(Math.toRadians(LATITUDE)); + + double randomLat = LATITUDE + latOffset; + double randomLng = LONGITUDE + lngOffset; + + return Map.of("latitude", randomLat, "longitude", randomLng); + } +} From 9a9dc0164d2e923e628a4a37c2b0a035310904b2 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 12 Aug 2025 14:05:18 +0900 Subject: [PATCH 08/23] =?UTF-8?q?[fix]=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/wayble/server/direction/init/GraphInit.java | 8 ++------ .../server/direction/service/WaybleDijkstraService.java | 2 +- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/main/java/com/wayble/server/direction/init/GraphInit.java b/src/main/java/com/wayble/server/direction/init/GraphInit.java index 01ee918b..7969aa52 100644 --- a/src/main/java/com/wayble/server/direction/init/GraphInit.java +++ b/src/main/java/com/wayble/server/direction/init/GraphInit.java @@ -29,7 +29,7 @@ public class GraphInit { private Map> adjacencyList; private Map nodeMap; private Map markerMap; - private final Set rampMakers; + private Set rampMarkers = Collections.emptySet(); @PostConstruct public void init() { @@ -67,7 +67,7 @@ private Map> buildAdjacencyList() { for (Edge edge : edges) { boolean isWaybleMarker = markerMap.containsKey(edge.from()) || markerMap.containsKey(edge.to()); - boolean isRamp = rampMakers.contains(edge.from()) || rampMakers.contains(edge.to()); + boolean isRamp = rampMarkers.contains(edge.from()) || rampMarkers.contains(edge.to()); double distance = isWaybleMarker ? edge.length() * 0.5 : edge.length(); if (isRamp) distance *= 5; @@ -110,8 +110,4 @@ public Map getMarkerMap() { public Map> getGraph() { return Collections.unmodifiableMap(adjacencyList); } - - public GraphInit(Set rampMakers) { - this.rampMakers = rampMakers; - } } diff --git a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java index 360cf5f0..8c847154 100644 --- a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java +++ b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java @@ -53,7 +53,7 @@ private List createPolyLine(List path) { .orElse(null); // 좌표 중복 제거 (동일 좌표가 연속될 시, 추가 X) - if (edge != null && edge.geometry() != null) { + if (edge != null && edge.geometry() != null && !edge.geometry().isEmpty()) { for (double[] coords : edge.geometry()) { deleteDuplicateCoords(polyline, coords); } From e879b4580445baadd707bd4664c7d67f8c5356b1 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 14:10:53 +0900 Subject: [PATCH 09/23] =?UTF-8?q?[feat]=20FacilityTest=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facility/WaybleFacilityDocumentRepository.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityDocumentRepository.java diff --git a/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityDocumentRepository.java b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityDocumentRepository.java new file mode 100644 index 00000000..ad5af65e --- /dev/null +++ b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityDocumentRepository.java @@ -0,0 +1,10 @@ +package com.wayble.server.explore.repository.facility; + +import com.wayble.server.explore.entity.WaybleFacilityDocument; +import org.springframework.data.elasticsearch.repository.ElasticsearchRepository; + +import java.util.List; + +public interface WaybleFacilityDocumentRepository extends ElasticsearchRepository { + List findAll(); +} From b8e3229a43bae00ae54a9f0e1292a2f1b5ad01e6 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 14:10:57 +0900 Subject: [PATCH 10/23] =?UTF-8?q?[feat]=20FacilityTest=20=ED=85=8C?= =?UTF-8?q?=EC=8A=A4=ED=8A=B8=20=EC=BD=94=EB=93=9C=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=99=95=EC=9D=B8=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleFacilityApiIntegrationTest.java | 81 +++++++++++++++++-- 1 file changed, 75 insertions(+), 6 deletions(-) diff --git a/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java index 0da66cf4..641c8431 100644 --- a/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java +++ b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java @@ -1,26 +1,28 @@ package com.wayble.server.explore; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.ObjectMapper; import com.wayble.server.common.config.security.jwt.JwtTokenProvider; +import com.wayble.server.explore.dto.facility.WaybleFacilityResponseDto; import com.wayble.server.explore.entity.FacilityType; import com.wayble.server.explore.entity.WaybleFacilityDocument; -import com.wayble.server.explore.entity.WaybleZoneDocument; import com.wayble.server.explore.repository.facility.WaybleFacilityDocumentRepository; import com.wayble.server.user.entity.Gender; import com.wayble.server.user.entity.LoginType; import com.wayble.server.user.entity.User; import com.wayble.server.user.entity.UserType; import com.wayble.server.user.repository.UserRepository; -import org.junit.jupiter.api.AfterAll; -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.api.*; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.data.elasticsearch.core.geo.GeoPoint; +import org.springframework.http.MediaType; import org.springframework.test.web.servlet.MockMvc; +import org.springframework.test.web.servlet.MvcResult; +import java.nio.charset.StandardCharsets; import java.time.LocalDate; import java.util.List; import java.util.Map; @@ -28,6 +30,8 @@ import java.util.UUID; import static org.assertj.core.api.Assertions.assertThat; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; @SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) @TestInstance(TestInstance.Lifecycle.PER_CLASS) @@ -57,7 +61,7 @@ public class WaybleFacilityApiIntegrationTest { private static final int SAMPLES = 100; - private static final String baseUrl = "/api/v1/wayble-zones/facilities"; + private static final String baseUrl = "/api/v1/facilities/search"; private Long userId; @@ -113,6 +117,71 @@ public void checkDataExists() { } } + + @Test + @DisplayName("좌표를 전달받아 가까운 경사로 조회 테스트") + public void findNearbyRampFacilities() throws Exception { + MvcResult result = mockMvc.perform(get(baseUrl) + .header("Authorization", "Bearer " + token) + .param("latitude", String.valueOf(LATITUDE)) + .param("longitude", String.valueOf(LONGITUDE)) + .param("facilityType", FacilityType.RAMP.name()) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + + String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + JsonNode root = objectMapper.readTree(json); + JsonNode dataNode = root.get("data"); + + System.out.println("==== 응답 결과 ===="); + System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); + + List dtoList = objectMapper.convertValue( + dataNode, + new TypeReference<>() {} + ); + + assertThat(dtoList).isNotEmpty(); + for (int i = 0; i < dtoList.size(); i++) { + WaybleFacilityResponseDto dto = dtoList.get(i); + System.out.println(dto); + } + } + + @Test + @DisplayName("좌표를 전달받아 가까운 엘리베이터 조회 테스트") + public void findNearbyElevatorFacilities() throws Exception { + MvcResult result = mockMvc.perform(get(baseUrl) + .header("Authorization", "Bearer " + token) + .param("latitude", String.valueOf(LATITUDE)) + .param("longitude", String.valueOf(LONGITUDE)) + .param("facilityType", FacilityType.ELEVATOR.name()) + .accept(MediaType.APPLICATION_JSON) + ) + .andExpect(status().is2xxSuccessful()) + .andReturn(); + + String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); + JsonNode root = objectMapper.readTree(json); + JsonNode dataNode = root.get("data"); + + System.out.println("==== 응답 결과 ===="); + System.out.println(objectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(objectMapper.readTree(json))); + + List dtoList = objectMapper.convertValue( + dataNode, + new TypeReference<>() {} + ); + + assertThat(dtoList).isNotEmpty(); + for (int i = 0; i < dtoList.size(); i++) { + WaybleFacilityResponseDto dto = dtoList.get(i); + System.out.println(dto); + } + } + private double haversine(double lat1, double lon1, double lat2, double lon2) { final int R = 6_371; // 지구 반지름 (km) double dLat = Math.toRadians(lat2 - lat1); From 0484f6cc8458a1aaf83eac2679ec83861cf2cb45 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 14:11:22 +0900 Subject: [PATCH 11/23] =?UTF-8?q?[feat]=20Facility=20=EA=B1=B0=EB=A6=AC?= =?UTF-8?q?=EC=88=9C=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EA=B5=AC=ED=98=84=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../facility/WaybleFacilityQuerySearchRepository.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java index d3ba130f..bb794d46 100644 --- a/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java +++ b/src/main/java/com/wayble/server/explore/repository/facility/WaybleFacilityQuerySearchRepository.java @@ -31,7 +31,7 @@ public class WaybleFacilityQuerySearchRepository { public List findNearbyFacilitiesByType( WaybleFacilityConditionDto condition) { - double radius = 5.0; // 기본 반경 5km + double radius = 10.0; // 기본 반경 5km String radiusWithUnit = radius + "km"; // 시설 타입에 따른 쿼리 조건 생성 From 75854757e8e2d212fc20a36a915dbc91d21527a7 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 14:14:25 +0900 Subject: [PATCH 12/23] =?UTF-8?q?[feat]=20Facility=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=ED=85=8C=EC=8A=A4=ED=8A=B8=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleFacilityApiIntegrationTest.java | 60 +++++++++++++++++-- 1 file changed, 56 insertions(+), 4 deletions(-) diff --git a/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java index 641c8431..a12691d4 100644 --- a/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java +++ b/src/test/java/com/wayble/server/explore/WaybleFacilityApiIntegrationTest.java @@ -144,10 +144,36 @@ public void findNearbyRampFacilities() throws Exception { ); assertThat(dtoList).isNotEmpty(); - for (int i = 0; i < dtoList.size(); i++) { - WaybleFacilityResponseDto dto = dtoList.get(i); + + // null 값 검증 + for (WaybleFacilityResponseDto dto : dtoList) { + assertThat(dto.latitude()).isNotNull(); + assertThat(dto.longitude()).isNotNull(); + assertThat(dto.facilityType()).isNotNull(); + assertThat(dto.facilityType()).isEqualTo(FacilityType.RAMP); System.out.println(dto); } + + // 거리순 정렬 검증 + if (dtoList.size() > 1) { + for (int i = 0; i < dtoList.size() - 1; i++) { + WaybleFacilityResponseDto current = dtoList.get(i); + WaybleFacilityResponseDto next = dtoList.get(i + 1); + + double currentDistance = haversine(LATITUDE, LONGITUDE, + current.latitude(), current.longitude()); + double nextDistance = haversine(LATITUDE, LONGITUDE, + next.latitude(), next.longitude()); + + assertThat(currentDistance).isLessThanOrEqualTo(nextDistance); + System.out.printf("Index %d: Distance = %.3f km%n", i, currentDistance); + } + // 마지막 요소의 거리도 출력 + WaybleFacilityResponseDto last = dtoList.get(dtoList.size() - 1); + double lastDistance = haversine(LATITUDE, LONGITUDE, + last.latitude(), last.longitude()); + System.out.printf("Index %d: Distance = %.3f km%n", dtoList.size() - 1, lastDistance); + } } @Test @@ -176,10 +202,36 @@ public void findNearbyElevatorFacilities() throws Exception { ); assertThat(dtoList).isNotEmpty(); - for (int i = 0; i < dtoList.size(); i++) { - WaybleFacilityResponseDto dto = dtoList.get(i); + + // null 값 검증 + for (WaybleFacilityResponseDto dto : dtoList) { + assertThat(dto.latitude()).isNotNull(); + assertThat(dto.longitude()).isNotNull(); + assertThat(dto.facilityType()).isNotNull(); + assertThat(dto.facilityType()).isEqualTo(FacilityType.ELEVATOR); System.out.println(dto); } + + // 거리순 정렬 검증 + if (dtoList.size() > 1) { + for (int i = 0; i < dtoList.size() - 1; i++) { + WaybleFacilityResponseDto current = dtoList.get(i); + WaybleFacilityResponseDto next = dtoList.get(i + 1); + + double currentDistance = haversine(LATITUDE, LONGITUDE, + current.latitude(), current.longitude()); + double nextDistance = haversine(LATITUDE, LONGITUDE, + next.latitude(), next.longitude()); + + assertThat(currentDistance).isLessThanOrEqualTo(nextDistance); + System.out.printf("Index %d: Distance = %.3f km%n", i, currentDistance); + } + // 마지막 요소의 거리도 출력 + WaybleFacilityResponseDto last = dtoList.get(dtoList.size() - 1); + double lastDistance = haversine(LATITUDE, LONGITUDE, + last.latitude(), last.longitude()); + System.out.printf("Index %d: Distance = %.3f km%n", dtoList.size() - 1, lastDistance); + } } private double haversine(double lat1, double lon1, double lat2, double lon2) { From e46e73ad44c5402875b7e43c2a40c2e0bc51a5fa Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 12 Aug 2025 14:26:08 +0900 Subject: [PATCH 13/23] =?UTF-8?q?[fix]=20=EC=83=81=EC=88=98=EB=A1=9C=20?= =?UTF-8?q?=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/wayble/server/direction/init/GraphInit.java | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/wayble/server/direction/init/GraphInit.java b/src/main/java/com/wayble/server/direction/init/GraphInit.java index 7969aa52..9aa13f6b 100644 --- a/src/main/java/com/wayble/server/direction/init/GraphInit.java +++ b/src/main/java/com/wayble/server/direction/init/GraphInit.java @@ -31,6 +31,9 @@ public class GraphInit { private Map markerMap; private Set rampMarkers = Collections.emptySet(); + private static final double RAMP_PENALTY = 5.0; + private static final double MARKER_DISCOUNT = 0.5; + @PostConstruct public void init() { ObjectMapper objectMapper = new ObjectMapper(); @@ -69,8 +72,10 @@ private Map> buildAdjacencyList() { boolean isWaybleMarker = markerMap.containsKey(edge.from()) || markerMap.containsKey(edge.to()); boolean isRamp = rampMarkers.contains(edge.from()) || rampMarkers.contains(edge.to()); - double distance = isWaybleMarker ? edge.length() * 0.5 : edge.length(); - if (isRamp) distance *= 5; + double distance = edge.length(); + + if (isRamp) distance *= RAMP_PENALTY; + else if (isWaybleMarker) distance *= MARKER_DISCOUNT; // 양방향 adjacencyList.computeIfAbsent(edge.from(), k -> new ArrayList<>()) From d99ca0a3c445b9834ac07a383de6cb23aa98aa7d Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 12 Aug 2025 14:37:05 +0900 Subject: [PATCH 14/23] =?UTF-8?q?[refactor]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=B6=94=EC=B2=9C=20=EA=B2=BD=EB=A1=9C=EA=B0=80=20?= =?UTF-8?q?30km=20=EC=9D=B4=EC=83=81=EC=9D=BC=20=EA=B2=BD=EC=9A=B0,=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EB=B0=98=ED=99=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wayble/server/direction/exception/WalkingErrorCase.java | 1 + .../server/direction/service/WaybleDijkstraService.java | 6 ++++++ 2 files changed, 7 insertions(+) diff --git a/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java b/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java index d0ba3c76..09413004 100644 --- a/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java +++ b/src/main/java/com/wayble/server/direction/exception/WalkingErrorCase.java @@ -12,6 +12,7 @@ public enum WalkingErrorCase implements ErrorCase { GRAPH_FILE_NOT_FOUND(500, 8002, "그래프 파일을 찾을 수 없습니다."), GRAPH_INIT_FAILED(500, 8003, "그래프 초기화에 실패했습니다."), NODE_NOT_FOUND(400, 8004, "해당 위도, 경도 근처의 노드가 존재하지 않습니다."), + DISTANCE_LIMIT_EXCEEDED(400, 8005, "경로가 허용 최대 거리(30km)를 초과하였습니다."), ; private final Integer httpStatusCode; diff --git a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java index 8c847154..422134dc 100644 --- a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java +++ b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java @@ -1,9 +1,11 @@ package com.wayble.server.direction.service; +import com.wayble.server.common.exception.ApplicationException; import com.wayble.server.direction.dto.response.WayblePathResponse; import com.wayble.server.direction.entity.Edge; import com.wayble.server.direction.entity.Node; import com.wayble.server.direction.entity.type.Type; +import com.wayble.server.direction.exception.WalkingErrorCase; import com.wayble.server.direction.init.GraphInit; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -18,12 +20,16 @@ public class WaybleDijkstraService { // 11cm의 오차 허용 private static final double TOLERANCE = 0.000001; + private static final double MAX_DISTANCE = 30000.0; public WayblePathResponse createWayblePath(long start, long end) { List path = dijkstra(start, end); Map markerMap = graphInit.getMarkerMap(); int totalDistance = (int) Math.round(calculateDistance(path)); + if (totalDistance >= MAX_DISTANCE) { + throw new ApplicationException(WalkingErrorCase.DISTANCE_LIMIT_EXCEEDED); + } // 노드 간 5초 대기 시간 추가 (횡단 보도, 보행자 상황 등 반영) int totalTime = (int) Math.round(calculateTime(path)) + path.size() * 5; From 80b0ff2aa018c874ef89bd9f2c09bde34ade98e8 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 14:45:42 +0900 Subject: [PATCH 15/23] =?UTF-8?q?[test]=20=EC=97=90=EB=9F=AC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=A0=80=EC=9E=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-develop.yml | 57 +++++++++++++++++---------- Dockerfile | 6 +++ src/main/resources/logback-spring.xml | 2 +- 3 files changed, 43 insertions(+), 22 deletions(-) diff --git a/.github/workflows/cd-develop.yml b/.github/workflows/cd-develop.yml index f4251d64..0b874e2a 100644 --- a/.github/workflows/cd-develop.yml +++ b/.github/workflows/cd-develop.yml @@ -1,8 +1,8 @@ name: CI/CD on: -# push: -# branches: [ "feature/seungmin" ] + push: + branches: [ "feature/seungmin" ] #push: # branches: [ "feature/seungin" ] @@ -69,16 +69,19 @@ jobs: run: | echo "🧹 Cleaning up all existing containers" - # Stop and remove specific containers + # Stop and remove specific containers (로그는 볼륨에 보존됨) sudo docker stop github-actions-demo || true sudo docker rm github-actions-demo || true sudo docker stop elasticsearch || true sudo docker rm elasticsearch || true + echo "📋 Ensuring log directory exists on host" + sudo mkdir -p /var/log/wayble + sudo chmod 755 /var/log/wayble - echo "🧯 Cleaning up unused Docker networks" - sudo docker system prune -f || true + echo "🧯 Cleaning up unused Docker networks (excluding volumes)" + sudo docker system prune -f --volumes=false || true - name: Create Docker network if not exists run: | @@ -184,23 +187,35 @@ jobs: exit 1 fi + # 로그 파일 상태 확인 + echo "=== Log Directory Status ===" + ls -la /var/log/wayble/ || echo "Log directory not found" + + if [ -f "/var/log/wayble/wayble-error.log" ]; then + echo "✅ Error log file exists" + echo "📊 Error log file size: $(du -h /var/log/wayble/wayble-error.log | cut -f1)" + echo "📅 Last modified: $(stat -c %y /var/log/wayble/wayble-error.log)" + else + echo "ℹ️ No error log file yet (normal for new deployment)" + fi + # ✅ 배포 성공 알림 (Discord) - - name: Send success webhook to Discord - if: success() - run: | - curl -H "Content-Type: application/json" \ - -X POST \ - -d "{\"content\": \"✅ EC2 배포 성공!\"}" \ - ${{ secrets.DISCORD_WEBHOOK_URL }} - - # ❌ 배포 실패 알림 (Discord) - - name: Send failure webhook to Discord - if: failure() - run: | - curl -H "Content-Type: application/json" \ - -X POST \ - -d "{\"content\": \"❌ EC2 배포 실패! 확인이 필요합니다.\"}" \ - ${{ secrets.DISCORD_WEBHOOK_URL }} +# - name: Send success webhook to Discord +# if: success() +# run: | +# curl -H "Content-Type: application/json" \ +# -X POST \ +# -d "{\"content\": \"✅ EC2 배포 성공!\"}" \ +# ${{ secrets.DISCORD_WEBHOOK_URL }} +# +# # ❌ 배포 실패 알림 (Discord) +# - name: Send failure webhook to Discord +# if: failure() +# run: | +# curl -H "Content-Type: application/json" \ +# -X POST \ +# -d "{\"content\": \"❌ EC2 배포 실패! 확인이 필요합니다.\"}" \ +# ${{ secrets.DISCORD_WEBHOOK_URL }} diff --git a/Dockerfile b/Dockerfile index ba724222..fb3ffbf6 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,6 +6,12 @@ FROM openjdk:17 # 인자 설정 - JAR_File ARG JAR_FILE=build/libs/*.jar +# 로그 디렉토리 생성 및 권한 설정 +RUN mkdir -p /app/logs && chmod 755 /app/logs + +# 작업 디렉토리 설정 +WORKDIR /app + # jar 파일 복제 COPY ${JAR_FILE} app.jar diff --git a/src/main/resources/logback-spring.xml b/src/main/resources/logback-spring.xml index ef41b877..246c2b0b 100644 --- a/src/main/resources/logback-spring.xml +++ b/src/main/resources/logback-spring.xml @@ -34,7 +34,7 @@ 50MB 90 1GB - true + false [%d{yyyy-MM-dd HH:mm:ss}] [%thread] %-5level %logger{36} - %msg%n%ex{2} From 0df86d852795eee84c1058c07cf250d5752f642e Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 14:55:19 +0900 Subject: [PATCH 16/23] =?UTF-8?q?[test]=20=EC=97=90=EB=9F=AC=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=20=EC=A0=80=EC=9E=A5=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-develop.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/cd-develop.yml b/.github/workflows/cd-develop.yml index 0b874e2a..e43377cc 100644 --- a/.github/workflows/cd-develop.yml +++ b/.github/workflows/cd-develop.yml @@ -219,7 +219,6 @@ jobs: - # on: #이 워크플로우가 언제 실행될지 트리거를 정의함. # pull_request: # types : [closed] #누군가가 Pull request를 닫았을 때 실행됨. From 1361a688fad1d25f137edba982f6ef5be4388c64 Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 12 Aug 2025 15:22:53 +0900 Subject: [PATCH 17/23] =?UTF-8?q?[fix]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=B6=94=EC=B2=9C=20=EA=B2=BD=EB=A1=9C=20=EC=B4=9D?= =?UTF-8?q?=20=EA=B1=B0=EB=A6=AC=20=EA=B3=84=EC=82=B0=20=EB=A1=9C=EC=A7=81?= =?UTF-8?q?=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../direction/service/WalkingService.java | 3 +++ .../service/WaybleDijkstraService.java | 23 +++++++++---------- 2 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/main/java/com/wayble/server/direction/service/WalkingService.java b/src/main/java/com/wayble/server/direction/service/WalkingService.java index 460c714b..f384e347 100644 --- a/src/main/java/com/wayble/server/direction/service/WalkingService.java +++ b/src/main/java/com/wayble/server/direction/service/WalkingService.java @@ -27,6 +27,8 @@ public class WalkingService { private final GraphInit graphInit; private final WaybleDijkstraService waybleDijkstraService; + private static final double NODE_SEARCH_RADIUS = 1000; + public TMapParsingResponse callTMapApi(TMapRequest request) { try { TMapResponse response = tMapClient.response(request); @@ -52,6 +54,7 @@ public WayblePathResponse findWayblePath( private long findNearestNode(double lat, double lon) { return graphInit.getNodeMap().values().stream() + .filter(node -> HaversineUtil.haversine(lat, lon, node.lat(), node.lon()) <= NODE_SEARCH_RADIUS) .min(Comparator.comparingDouble( node -> HaversineUtil.haversine(lat, lon, node.lat(), node.lon()) )) diff --git a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java index 422134dc..2e35026e 100644 --- a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java +++ b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java @@ -7,6 +7,7 @@ import com.wayble.server.direction.entity.type.Type; import com.wayble.server.direction.exception.WalkingErrorCase; import com.wayble.server.direction.init.GraphInit; +import com.wayble.server.direction.service.util.HaversineUtil; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -26,10 +27,13 @@ public WayblePathResponse createWayblePath(long start, long end) { List path = dijkstra(start, end); Map markerMap = graphInit.getMarkerMap(); - int totalDistance = (int) Math.round(calculateDistance(path)); + List polyline = createPolyLine(path); + int totalDistance = (int) Math.round(calculateDistance(polyline)); + if (totalDistance >= MAX_DISTANCE) { throw new ApplicationException(WalkingErrorCase.DISTANCE_LIMIT_EXCEEDED); } + // 노드 간 5초 대기 시간 추가 (횡단 보도, 보행자 상황 등 반영) int totalTime = (int) Math.round(calculateTime(path)) + path.size() * 5; @@ -40,8 +44,6 @@ public WayblePathResponse createWayblePath(long start, long end) { return new WayblePathResponse.WayblePoint(node.lat(), node.lon(), type); }).toList(); - List polyline = createPolyLine(path); - return WayblePathResponse.of(totalDistance, totalTime, wayblePoints, polyline); } @@ -115,17 +117,14 @@ private double calculateTime(List path) { return totalTime; } - private double calculateDistance(List path) { + private double calculateDistance(List polyline) { double totalDistance = 0.0; - for (int i = 0; i < path.size() - 1; i++) { - long from = path.get(i); - long to = path.get(i + 1); - totalDistance += graphInit.getGraph().getOrDefault(from, List.of()).stream() - .filter(edge -> edge.to() == to) - .findFirst() - .map(Edge::length) - .orElse(0.0); + for (int i = 1; i < polyline.size(); i++) { + totalDistance += HaversineUtil.haversine( + polyline.get(i - 1)[1], polyline.get(i - 1)[0], + polyline.get(i)[1], polyline.get(i)[0] + ); } return totalDistance; } From 7c93c294b146a95aa5a9fb6fef550958daa7dce0 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 15:26:04 +0900 Subject: [PATCH 18/23] =?UTF-8?q?[feat]=20=ED=81=B4=EB=9D=BC=EC=9D=B4?= =?UTF-8?q?=EC=96=B8=ED=8A=B8=20=EC=98=88=EC=99=B8=20=EB=94=94=EC=8A=A4?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20=EC=95=8C=EB=A6=BC=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=A0=9C=EC=99=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../exception/GlobalExceptionHandler.java | 131 ++++++++++++++++-- 1 file changed, 117 insertions(+), 14 deletions(-) diff --git a/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java b/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java index ce6ace69..ca67047c 100644 --- a/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java @@ -38,7 +38,7 @@ public class GlobalExceptionHandler { @ExceptionHandler(ApplicationException.class) public ResponseEntity handleApplicationException(ApplicationException e, WebRequest request) { - // 비즈니스 예외 로그 기록 (간결하게) + String path = ((ServletWebRequest) request).getRequest().getRequestURI(); String method = ((ServletWebRequest) request).getRequest().getMethod(); @@ -47,36 +47,31 @@ public ResponseEntity handleApplicationException(ApplicationExce CommonResponse commonResponse = CommonResponse.error(e.getErrorCase()); - HttpStatus status = HttpStatus.valueOf(e.getErrorCase().getHttpStatusCode()); - //sendToDiscord(e, request, status); - return ResponseEntity .status(e.getErrorCase().getHttpStatusCode()) .body(commonResponse); } - @ExceptionHandler(value = MethodArgumentNotValidException.class) - public ResponseEntity handleValidException(BindingResult bindingResult, - MethodArgumentNotValidException ex, - WebRequest request) { - String message = bindingResult.getAllErrors().get(0).getDefaultMessage(); + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity handleValidException(MethodArgumentNotValidException ex, WebRequest request) { + String message = ex.getBindingResult().getAllErrors().get(0).getDefaultMessage(); // 에러 로그 기록 String path = ((ServletWebRequest) request).getRequest().getRequestURI(); String method = ((ServletWebRequest) request).getRequest().getMethod(); String errorLocation = getErrorLocation(ex); - log.error("Validation Exception 발생 - Method: {}, Path: {}, Message: {}, Location: {}", - method, path, message, errorLocation, ex); + log.warn("Validation Exception - Method: {}, Path: {}, Message: {}, Location: {}", + method, path, message, errorLocation); CommonResponse commonResponse = CommonResponse.error(400, message); - - sendToDiscord(ex, request, HttpStatus.BAD_REQUEST); + return ResponseEntity .status(HttpStatus.BAD_REQUEST) .body(commonResponse); } + /** * 모든 예상하지 못한 예외 처리 */ @@ -102,11 +97,17 @@ private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status) String path = ((ServletWebRequest) request).getRequest().getRequestURI(); String timestamp = Instant.now().toString(); - if (!env.acceptsProfiles(Profiles.of("develop"))) { + if (!env.acceptsProfiles(Profiles.of("local"))) { log.info("현재 active 프로파일이 develop가 아니므로 Discord 알림을 보내지 않습니다."); return; } + // 특정 예외 타입 및 경로에 대한 Discord 알림 제외 + if (shouldSkipDiscordNotification(ex, path)) { + log.debug("Discord 알림 제외 - Exception: {}, Path: {}", ex.getClass().getSimpleName(), path); + return; + } + // Embed 필드 구성 DiscordWebhookPayload.Embed embed = new DiscordWebhookPayload.Embed( "🚨 서버 에러 발생", @@ -136,6 +137,108 @@ private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status) } } + /** + * Discord 알림을 보내지 않을 예외인지 판단 + */ + private boolean shouldSkipDiscordNotification(Exception ex, String path) { + String exceptionName = ex.getClass().getSimpleName(); + String message = ex.getMessage(); + + // 1. NoResourceFoundException 제외 (static resource 요청) + if ("NoResourceFoundException".equals(exceptionName)) { + return true; + } + + // 2. 특정 경로 패턴 제외 + if (isIgnoredPath(path)) { + return true; + } + + // 3. 봇이나 크롤러 요청으로 인한 에러 제외 + if (isBotOrCrawlerRequest(message)) { + return true; + } + + // 4. 기타 불필요한 예외들 + if (isIgnoredException(exceptionName, message)) { + return true; + } + + return false; + } + + /** + * 무시할 경로인지 확인 + */ + private boolean isIgnoredPath(String path) { + String[] ignoredPaths = { + "/favicon.ico", + "/index.html", + "/robots.txt", + "/sitemap.xml", + "/apple-touch-icon", + "/.well-known/", + "/wp-admin/", + "/admin/", + "/phpmyadmin/", + "/xmlrpc.php", + "/.env", + "/config.php" + }; + + for (String ignoredPath : ignoredPaths) { + if (path.contains(ignoredPath)) { + return true; + } + } + + return false; + } + + /** + * 봇이나 크롤러 요청인지 확인 + */ + private boolean isBotOrCrawlerRequest(String message) { + if (message == null) return false; + + String[] botIndicators = { + "No static resource", + "Could not resolve view", + "favicon", + "robots.txt" + }; + + for (String indicator : botIndicators) { + if (message.contains(indicator)) { + return true; + } + } + + return false; + } + + /** + * 무시할 예외인지 확인 + */ + private boolean isIgnoredException(String exceptionName, String message) { + // 클라이언트 연결 종료 관련 + if ("ClientAbortException".equals(exceptionName) || + "BrokenPipeException".equals(exceptionName)) { + return true; + } + + // 타임아웃 관련 (너무 빈번한 경우) + if (message != null && ( + message.contains("Connection timed out") || + message.contains("Read timed out") || + message.contains("Connection reset") + )) { + return true; + } + + return false; + } + /** * 예외의 스택트레이스에서 실제 에러 발생 위치를 추출 */ From d7cf1e1c8afd8f2cd46065d5ada7b9500f031435 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 15:27:49 +0900 Subject: [PATCH 19/23] =?UTF-8?q?[chore]=20workflow=20=EC=9B=90=EB=B3=B5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/cd-develop.yml | 36 ++++++++++++++++---------------- 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/workflows/cd-develop.yml b/.github/workflows/cd-develop.yml index e43377cc..48881ed1 100644 --- a/.github/workflows/cd-develop.yml +++ b/.github/workflows/cd-develop.yml @@ -1,8 +1,8 @@ name: CI/CD on: - push: - branches: [ "feature/seungmin" ] +# push: +# branches: [ "feature/seungmin" ] #push: # branches: [ "feature/seungin" ] @@ -200,22 +200,22 @@ jobs: fi # ✅ 배포 성공 알림 (Discord) -# - name: Send success webhook to Discord -# if: success() -# run: | -# curl -H "Content-Type: application/json" \ -# -X POST \ -# -d "{\"content\": \"✅ EC2 배포 성공!\"}" \ -# ${{ secrets.DISCORD_WEBHOOK_URL }} -# -# # ❌ 배포 실패 알림 (Discord) -# - name: Send failure webhook to Discord -# if: failure() -# run: | -# curl -H "Content-Type: application/json" \ -# -X POST \ -# -d "{\"content\": \"❌ EC2 배포 실패! 확인이 필요합니다.\"}" \ -# ${{ secrets.DISCORD_WEBHOOK_URL }} + - name: Send success webhook to Discord + if: success() + run: | + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{\"content\": \"✅ EC2 배포 성공!\"}" \ + ${{ secrets.DISCORD_WEBHOOK_URL }} + + # ❌ 배포 실패 알림 (Discord) + - name: Send failure webhook to Discord + if: failure() + run: | + curl -H "Content-Type: application/json" \ + -X POST \ + -d "{\"content\": \"❌ EC2 배포 실패! 확인이 필요합니다.\"}" \ + ${{ secrets.DISCORD_WEBHOOK_URL }} From 42b18ffa87e8eacb54a7560d195d8b68466e6e3b Mon Sep 17 00:00:00 2001 From: zyovn Date: Tue, 12 Aug 2025 15:35:51 +0900 Subject: [PATCH 20/23] =?UTF-8?q?[fix]=20=ED=94=BC=EB=93=9C=EB=B0=B1=20?= =?UTF-8?q?=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/direction/service/WaybleDijkstraService.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java index 2e35026e..3a5b837c 100644 --- a/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java +++ b/src/main/java/com/wayble/server/direction/service/WaybleDijkstraService.java @@ -28,11 +28,12 @@ public WayblePathResponse createWayblePath(long start, long end) { Map markerMap = graphInit.getMarkerMap(); List polyline = createPolyLine(path); - int totalDistance = (int) Math.round(calculateDistance(polyline)); + double totalDistanceMeters = calculateDistance(polyline); - if (totalDistance >= MAX_DISTANCE) { + if (totalDistanceMeters >= MAX_DISTANCE) { throw new ApplicationException(WalkingErrorCase.DISTANCE_LIMIT_EXCEEDED); } + int totalDistance = (int) Math.round(totalDistanceMeters); // 노드 간 5초 대기 시간 추가 (횡단 보도, 보행자 상황 등 반영) int totalTime = (int) Math.round(calculateTime(path)) + path.size() * 5; From a6efa3912e86df0f2d8b4ba534486fa4d1e9b2dd Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 16:12:44 +0900 Subject: [PATCH 21/23] =?UTF-8?q?[refactor]=20=EC=97=94=EB=93=9C=ED=8F=AC?= =?UTF-8?q?=EC=9D=B8=ED=8A=B8=EC=97=90=EC=84=9C=20userId=20=EC=A0=9C?= =?UTF-8?q?=EA=B1=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleZoneRecommendController.java | 4 +++- .../WaybleZoneRecommendConditionDto.java | 5 +--- .../user/controller/UserPlaceController.java | 24 +++---------------- 3 files changed, 7 insertions(+), 26 deletions(-) diff --git a/src/main/java/com/wayble/server/explore/controller/WaybleZoneRecommendController.java b/src/main/java/com/wayble/server/explore/controller/WaybleZoneRecommendController.java index a757b94d..886f7d23 100644 --- a/src/main/java/com/wayble/server/explore/controller/WaybleZoneRecommendController.java +++ b/src/main/java/com/wayble/server/explore/controller/WaybleZoneRecommendController.java @@ -6,6 +6,7 @@ import com.wayble.server.explore.service.WaybleZoneRecommendService; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; @@ -24,8 +25,9 @@ public CommonResponse> getWaybleZonePersona @Valid @ModelAttribute WaybleZoneRecommendConditionDto conditionDto, @RequestParam(name = "size", defaultValue = "1") int size) { + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); List result = waybleZoneRecommendService.getWaybleZonePersonalRecommend( - conditionDto.userId(), + userId, conditionDto.latitude(), conditionDto.longitude(), size diff --git a/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendConditionDto.java b/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendConditionDto.java index a4fa46cd..30f7333c 100644 --- a/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendConditionDto.java +++ b/src/main/java/com/wayble/server/explore/dto/recommend/WaybleZoneRecommendConditionDto.java @@ -15,9 +15,6 @@ public record WaybleZoneRecommendConditionDto( @DecimalMin(value = "-180.0", message = "경도는 -180.0 이상이어야 합니다.") @DecimalMax(value = "180.0", message = "경도는 180.0 이하여야 합니다.") @NotNull(message = "경도 입력은 필수입니다.") - Double longitude, - - @NotNull(message = "유저 ID는 필수입니다.") - Long userId + Double longitude ) { } diff --git a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java index a944dc54..d17a09ea 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -18,13 +18,12 @@ import java.util.List; @RestController -@RequestMapping("/api/v1/users/{userId}/places") +@RequestMapping("/api/v1/users/places") @RequiredArgsConstructor public class UserPlaceController { private final UserPlaceService userPlaceService; - @PostMapping @Operation(summary = "유저 장소 저장", description = "유저가 웨이블존을 장소로 저장합니다.") @ApiResponses({ @@ -34,18 +33,9 @@ public class UserPlaceController { @ApiResponse(responseCode = "403", description = "권한이 없습니다.") }) public CommonResponse saveUserPlace( - @PathVariable Long userId, @RequestBody @Valid UserPlaceRequestDto request ) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (!(authentication.getPrincipal() instanceof Long)) { - throw new ApplicationException(UserErrorCase.FORBIDDEN); - } - Long tokenUserId = (Long) authentication.getPrincipal(); - if (!userId.equals(tokenUserId)) { - throw new ApplicationException(UserErrorCase.FORBIDDEN); - } - + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); userPlaceService.saveUserPlace(userId, request); // userId 파라미터로 넘김 return CommonResponse.success("장소가 저장되었습니다."); } @@ -61,16 +51,8 @@ public CommonResponse saveUserPlace( @ApiResponse(responseCode = "403", description = "권한이 없습니다.") }) public CommonResponse> getUserPlaces( - @PathVariable Long userId ) { - Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); - if (!(authentication.getPrincipal() instanceof Long)) { - throw new ApplicationException(UserErrorCase.FORBIDDEN); - } - Long tokenUserId = (Long) authentication.getPrincipal(); - if (!userId.equals(tokenUserId)) { - throw new ApplicationException(UserErrorCase.FORBIDDEN); - } + Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); List places = userPlaceService.getUserPlaces(userId); return CommonResponse.success(places); } From 6341141db0449ba3462e365c64a87f4008244d5b Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 16:15:16 +0900 Subject: [PATCH 22/23] =?UTF-8?q?[fix]=20=EB=94=94=EC=8A=A4=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=95=8C=EB=A6=BC=20=ED=94=84=EB=A1=9C=ED=95=84?= =?UTF-8?q?=EC=9D=B4=20local=EB=A1=9C=20=EB=90=98=EC=96=B4=20=EC=9E=88?= =?UTF-8?q?=EC=96=B4=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wayble/server/common/exception/GlobalExceptionHandler.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java b/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java index ca67047c..22288ba5 100644 --- a/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java +++ b/src/main/java/com/wayble/server/common/exception/GlobalExceptionHandler.java @@ -97,7 +97,7 @@ private void sendToDiscord(Exception ex, WebRequest request, HttpStatus status) String path = ((ServletWebRequest) request).getRequest().getRequestURI(); String timestamp = Instant.now().toString(); - if (!env.acceptsProfiles(Profiles.of("local"))) { + if (!env.acceptsProfiles(Profiles.of("develop"))) { log.info("현재 active 프로파일이 develop가 아니므로 Discord 알림을 보내지 않습니다."); return; } From 6075a523d3cb9f4c047bcb7db5a3ac1eab69ac78 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Tue, 12 Aug 2025 16:22:58 +0900 Subject: [PATCH 23/23] =?UTF-8?q?[refactor]=20dto=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=ED=98=95=EC=8B=9D=EC=9D=84=20camelCase=EB=A1=9C=20=ED=86=B5?= =?UTF-8?q?=EC=9D=BC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wayble/server/user/dto/UserPlaceListResponseDto.java | 8 ++++---- .../com/wayble/server/user/service/UserPlaceService.java | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceListResponseDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceListResponseDto.java index 73f155b9..9a8d79ee 100644 --- a/src/main/java/com/wayble/server/user/dto/UserPlaceListResponseDto.java +++ b/src/main/java/com/wayble/server/user/dto/UserPlaceListResponseDto.java @@ -6,17 +6,17 @@ @Builder public record UserPlaceListResponseDto( - Long place_id, + Long placeId, String title, - WaybleZoneDto wayble_zone + WaybleZoneDto waybleZone ) { @Builder public record WaybleZoneDto( - Long wayble_zone_id, + Long waybleZoneId, String name, String category, double rating, String address, - String image_url + String imageUrl ) {} } \ No newline at end of file diff --git a/src/main/java/com/wayble/server/user/service/UserPlaceService.java b/src/main/java/com/wayble/server/user/service/UserPlaceService.java index 6581e153..8200715e 100644 --- a/src/main/java/com/wayble/server/user/service/UserPlaceService.java +++ b/src/main/java/com/wayble/server/user/service/UserPlaceService.java @@ -78,16 +78,16 @@ public List getUserPlaces(Long userId) { String imageUrl = waybleZone.getMainImageUrl(); return UserPlaceListResponseDto.builder() - .place_id(userPlace.getId()) + .placeId(userPlace.getId()) .title(userPlace.getTitle()) - .wayble_zone( + .waybleZone( UserPlaceListResponseDto.WaybleZoneDto.builder() - .wayble_zone_id(waybleZone.getId()) + .waybleZoneId(waybleZone.getId()) .name(waybleZone.getZoneName()) .category(waybleZone.getZoneType().toString()) .rating(waybleZone.getRating()) .address(waybleZone.getAddress().toFullAddress()) - .image_url(imageUrl) + .imageUrl(imageUrl) .build() ) .build();