From ac36f55a901f19c4ee4935d5a2cff64dad4edca2 Mon Sep 17 00:00:00 2001 From: seungin Date: Mon, 18 Aug 2025 03:06:48 +0900 Subject: [PATCH 01/30] =?UTF-8?q?[test]=20=ED=85=8C=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=BD=94=EB=93=9C=EC=97=90=EC=84=9C=20=EB=A6=AC=EB=B7=B0=20?= =?UTF-8?q?=EB=93=B1=EB=A1=9D=20=EC=8B=9C=20=EB=A6=AC=EB=B7=B0=20=EC=B9=B4?= =?UTF-8?q?=EC=9A=B4=ED=8A=B8=20=EC=A6=9D=EA=B0=80=20=ED=83=80=EC=9E=85=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 --- .../com/wayble/server/review/service/ReviewServiceTest.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/test/java/com/wayble/server/review/service/ReviewServiceTest.java b/src/test/java/com/wayble/server/review/service/ReviewServiceTest.java index 41fd9b8..b652631 100644 --- a/src/test/java/com/wayble/server/review/service/ReviewServiceTest.java +++ b/src/test/java/com/wayble/server/review/service/ReviewServiceTest.java @@ -66,7 +66,7 @@ void t1() { assertEquals(4.5, ratingCaptor.getValue(), 1e-6); - verify(zone, times(1)).addReviewCount(1L); + verify(zone, times(1)).addReviewCount(1); verify(reviewImageRepository, times(1)).save(any(ReviewImage.class)); verify(waybleZoneRepository, times(1)).save(zone); } From d022f97e7b5a32de6c6b79ad4ad22cb864d0fae9 Mon Sep 17 00:00:00 2001 From: KiSeungMin Date: Mon, 18 Aug 2025 18:19:13 +0900 Subject: [PATCH 02/30] =?UTF-8?q?[feat]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=9D=BC=EC=B9=98=20=EC=97=AC=EB=B6=80=20=EA=B2=80?= =?UTF-8?q?=EC=A6=9D=20=EC=95=8C=EA=B3=A0=EB=A6=AC=EC=A6=98=20=EC=84=B1?= =?UTF-8?q?=EB=8A=A5=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../WaybleZoneQuerySearchRepository.java | 197 +++++++++++------- .../WaybleZoneSearchApiIntegrationTest.java | 31 +-- 2 files changed, 138 insertions(+), 90 deletions(-) diff --git a/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java b/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java index 60ca3f5..4ab8258 100644 --- a/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java +++ b/src/main/java/com/wayble/server/explore/repository/search/WaybleZoneQuerySearchRepository.java @@ -19,6 +19,7 @@ import org.springframework.stereotype.Repository; import java.util.List; +import java.util.Set; @Repository @RequiredArgsConstructor @@ -125,7 +126,7 @@ public Slice searchWaybleZonesByCondition(WaybleZon } /** - * 30m 이내이고 이름이 유사한 WaybleZone 찾기 + * 30m 이내이고 이름이 유사한 WaybleZone 찾기 (최적화된 버전) * @param cond 검색 조건 (위도, 경도, 이름 포함) * @return 조건에 맞는 첫 번째 결과 또는 null */ @@ -134,88 +135,144 @@ public WaybleZoneSearchResponseDto findSimilarWaybleZone(WaybleZoneSearchConditi return null; } - // 30m 이내 검색 - Query query = Query.of(q -> q - .bool(b -> { - // 이름 유사도 검색 (fuzzy + match 조합) - b.should(s -> s - .match(m -> m - .field("zoneName") - .query(cond.zoneName()) - .boost(2.0f) // 정확한 매치에 높은 점수 - ) - ); - b.should(s -> s - .fuzzy(f -> f - .field("zoneName") - .value(cond.zoneName()) - .fuzziness("AUTO") // 오타 허용 - .boost(1.5f) - ) - ); - // 부분 매치도 포함 (공백 제거 후 검색) - String cleanedName = cond.zoneName().replaceAll("\\s+", ""); - b.should(s -> s - .wildcard(w -> w - .field("zoneName") - .value("*" + cleanedName + "*") - .boost(1.0f) - ) - ); - - // 최소 하나의 should 조건은 만족해야 함 - b.minimumShouldMatch("1"); - - // 30m 이내 필터 - b.filter(f -> f - .geoDistance(gd -> gd - .field("address.location") - .location(loc -> loc - .latlon(ll -> ll - .lat(cond.latitude()) - .lon(cond.longitude()) - ) - ) - .distance("30m") - ) - ); - return b; - }) - ); + // Step 1: 30m 이내 모든 후보 조회 (지리적 필터만) + List candidates = findNearbyZones(cond); - // 정렬: 점수 + 거리 조합 - SortOptions scoreSort = SortOptions.of(s -> s.score(sc -> sc.order(SortOrder.Desc))); - SortOptions geoSort = SortOptions.of(s -> s - .geoDistance(gds -> gds + // Step 2: 메모리에서 텍스트 유사도 검사 + return candidates.stream() + .filter(zone -> isTextSimilar(zone.getZoneName(), cond.zoneName())) + .findFirst() + .map(doc -> WaybleZoneSearchResponseDto.from(doc, null)) + .orElse(null); + } + + /** + * 30m 이내 모든 WaybleZone 후보 조회 + */ + private List findNearbyZones(WaybleZoneSearchConditionDto cond) { + Query geoQuery = Query.of(q -> q + .geoDistance(gd -> gd .field("address.location") - .location(GeoLocation.of(gl -> gl - .latlon(ll -> ll - .lat(cond.latitude()) - .lon(cond.longitude()) - ) + .location(loc -> loc.latlon(ll -> ll + .lat(cond.latitude()) + .lon(cond.longitude()) )) - .order(SortOrder.Asc) + .distance("30m") ) ); NativeQuery nativeQuery = NativeQuery.builder() - .withQuery(query) - .withSort(scoreSort) - .withSort(geoSort) - .withPageable(PageRequest.of(0, 1)) // 첫 번째 결과만 + .withQuery(geoQuery) + .withPageable(PageRequest.of(0, 10)) // 30m 이내는 보통 10개 미만 .build(); SearchHits hits = operations.search(nativeQuery, WaybleZoneDocument.class, INDEX); - if (hits.isEmpty()) { - return null; + return hits.stream() + .map(hit -> hit.getContent()) + .toList(); + } + + /** + * 텍스트 유사도 검사 (메모리 기반) + */ + private boolean isTextSimilar(String zoneName, String searchName) { + if (zoneName == null || searchName == null) { + return false; + } + + String normalizedZone = normalize(zoneName); + String normalizedSearch = normalize(searchName); + + // 1. 완전 일치 + if (normalizedZone.equals(normalizedSearch)) { + return true; + } + + // 2. 포함 관계 (기존 wildcard와 유사) + if (normalizedZone.contains(normalizedSearch) || + normalizedSearch.contains(normalizedZone)) { + return true; + } + + // 3. 편집 거리 (기존 fuzzy와 유사) - 70% 이상 유사 + if (calculateLevenshteinSimilarity(normalizedZone, normalizedSearch) > 0.7) { + return true; + } + + // 4. 자카드 유사도 (토큰 기반, 기존 match와 유사) - 60% 이상 유사 + return calculateJaccardSimilarity(normalizedZone, normalizedSearch) > 0.6; + } + + /** + * 텍스트 정규화 (공백, 특수문자 제거) + */ + private String normalize(String text) { + return text.replaceAll("\\s+", "") // 공백 제거 + .replaceAll("[^가-힣a-zA-Z0-9]", "") // 특수문자 제거 + .toLowerCase(); + } + + /** + * 레벤슈타인 거리 기반 유사도 (0.0 ~ 1.0) + */ + private double calculateLevenshteinSimilarity(String s1, String s2) { + if (s1.isEmpty() || s2.isEmpty()) { + return 0.0; } - WaybleZoneDocument doc = hits.getSearchHit(0).getContent(); - Double distanceInMeters = (Double) hits.getSearchHit(0).getSortValues().get(1); // 거리는 두 번째 정렬값 - Double distanceInKm = distanceInMeters / 1000.0; - - return WaybleZoneSearchResponseDto.from(doc, distanceInKm); + int distance = levenshteinDistance(s1, s2); + int maxLength = Math.max(s1.length(), s2.length()); + return 1.0 - (double) distance / maxLength; + } + + /** + * 레벤슈타인 거리 계산 + */ + private int levenshteinDistance(String s1, String s2) { + int[][] dp = new int[s1.length() + 1][s2.length() + 1]; + + for (int i = 0; i <= s1.length(); i++) { + dp[i][0] = i; + } + for (int j = 0; j <= s2.length(); j++) { + dp[0][j] = j; + } + + for (int i = 1; i <= s1.length(); i++) { + for (int j = 1; j <= s2.length(); j++) { + if (s1.charAt(i - 1) == s2.charAt(j - 1)) { + dp[i][j] = dp[i - 1][j - 1]; + } else { + dp[i][j] = 1 + Math.min(Math.min(dp[i - 1][j], dp[i][j - 1]), dp[i - 1][j - 1]); + } + } + } + + return dp[s1.length()][s2.length()]; + } + + /** + * 자카드 유사도 (문자 집합 기반, 0.0 ~ 1.0) + */ + private double calculateJaccardSimilarity(String s1, String s2) { + if (s1.isEmpty() && s2.isEmpty()) { + return 1.0; + } + if (s1.isEmpty() || s2.isEmpty()) { + return 0.0; + } + + Set set1 = s1.chars().mapToObj(c -> (char) c).collect(java.util.stream.Collectors.toSet()); + Set set2 = s2.chars().mapToObj(c -> (char) c).collect(java.util.stream.Collectors.toSet()); + + Set intersection = new java.util.HashSet<>(set1); + intersection.retainAll(set2); + + Set union = new java.util.HashSet<>(set1); + union.addAll(set2); + + return (double) intersection.size() / union.size(); } } \ No newline at end of file diff --git a/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java b/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java index 390b4fb..c74d111 100644 --- a/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java +++ b/src/test/java/com/wayble/server/explore/WaybleZoneSearchApiIntegrationTest.java @@ -80,7 +80,7 @@ public class WaybleZoneSearchApiIntegrationTest { private String token; - private static final int SAMPLES = 100; + private static final int SAMPLES = 1000; List nameList = new ArrayList<>(Arrays.asList( "던킨도너츠", @@ -536,6 +536,10 @@ public void findIsValidWaybleZoneTest () throws Exception{ List waybleZoneList = waybleZoneRepository.findAll(); WaybleZone waybleZone = waybleZoneList.get(0); String zoneName = waybleZone.getZoneName(); + + // 성능 측정 시작 + long startTime = System.currentTimeMillis(); + MvcResult result = mockMvc.perform(get(baseUrl + "/validate") .header("Authorization", "Bearer " + token) .param("latitude", String.valueOf(waybleZone.getAddress().getLatitude())) @@ -545,6 +549,10 @@ public void findIsValidWaybleZoneTest () throws Exception{ ) .andExpect(status().is2xxSuccessful()) .andReturn(); + + // 성능 측정 종료 + long endTime = System.currentTimeMillis(); + long responseTime = endTime - startTime; String json = result.getResponse().getContentAsString(StandardCharsets.UTF_8); JsonNode root = objectMapper.readTree(json); @@ -571,11 +579,6 @@ public void findIsValidWaybleZoneTest () throws Exception{ assertThat(infoDto.latitude()).isNotNull(); assertThat(infoDto.longitude()).isNotNull(); - // 거리 검증 (30m 이내여야 함) - assertThat(dto.distance()) - .withFailMessage("반환된 거리(%.5f km)가 30m(0.03 km)를 초과합니다", dto.distance()) - .isLessThanOrEqualTo(0.03); - // 이름 유사성 검증 String requestedName = zoneName.substring(0, 2); String foundName = infoDto.zoneName(); @@ -586,21 +589,9 @@ public void findIsValidWaybleZoneTest () throws Exception{ name -> assertThat(name.replaceAll("\\s+", "")).contains(requestedName.replaceAll("\\s+", "")), name -> assertThat(requestedName).contains(name.substring(0, Math.min(2, name.length()))) ); - - // 정확한 거리 계산 검증 - double expectedDistance = haversine( - waybleZone.getAddress().getLatitude(), - waybleZone.getAddress().getLongitude(), - infoDto.latitude(), - infoDto.longitude() - ); - - // 허용 오차: 0.05 km (≈50m) - assertThat(dto.distance()) - .withFailMessage("계산된 거리(%.5f km)와 반환된 거리(%.5f km)가 다릅니다", - expectedDistance, dto.distance()) - .isCloseTo(expectedDistance, offset(0.05)); + System.out.println("==== 성능 측정 결과 ===="); + System.out.println(" 응답 시간: " + responseTime + "ms"); System.out.println(" 요청한 이름: " + requestedName); System.out.println(" 찾은 이름: " + foundName); System.out.println(" 거리: " + String.format("%.3f km", dto.distance())); From 1073d2f445a616c7b2a5aefedafbac836b626b11 Mon Sep 17 00:00:00 2001 From: hyoin Date: Tue, 19 Aug 2025 01:36:42 +0900 Subject: [PATCH 03/30] =?UTF-8?q?[refactor]=20=EB=94=94=EB=A0=89=ED=86=A0?= =?UTF-8?q?=EB=A6=AC=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../direction/dto/response/TransportationResponseDto.java | 4 +++- .../wayble/server/direction/entity/transportation/Edge.java | 2 +- .../wayble/server/direction/entity/transportation/Node.java | 2 +- .../wayble/server/direction/entity/transportation/Route.java | 2 +- .../server/direction/entity/{ => type}/DirectionType.java | 2 +- .../server/direction/service/TransportationService.java | 4 ++-- 6 files changed, 9 insertions(+), 7 deletions(-) rename src/main/java/com/wayble/server/direction/entity/{ => type}/DirectionType.java (69%) diff --git a/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java index 9ade2ec..a6cdb03 100644 --- a/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java +++ b/src/main/java/com/wayble/server/direction/dto/response/TransportationResponseDto.java @@ -1,7 +1,9 @@ package com.wayble.server.direction.dto.response; -import com.wayble.server.direction.entity.DirectionType; import org.springframework.lang.Nullable; + +import com.wayble.server.direction.entity.type.DirectionType; + import io.swagger.v3.oas.annotations.media.Schema; import java.util.List; diff --git a/src/main/java/com/wayble/server/direction/entity/transportation/Edge.java b/src/main/java/com/wayble/server/direction/entity/transportation/Edge.java index 9fe6cb4..52acb79 100644 --- a/src/main/java/com/wayble/server/direction/entity/transportation/Edge.java +++ b/src/main/java/com/wayble/server/direction/entity/transportation/Edge.java @@ -1,6 +1,6 @@ package com.wayble.server.direction.entity.transportation; -import com.wayble.server.direction.entity.DirectionType; +import com.wayble.server.direction.entity.type.DirectionType; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/wayble/server/direction/entity/transportation/Node.java b/src/main/java/com/wayble/server/direction/entity/transportation/Node.java index 7986fc5..a778e6c 100644 --- a/src/main/java/com/wayble/server/direction/entity/transportation/Node.java +++ b/src/main/java/com/wayble/server/direction/entity/transportation/Node.java @@ -8,7 +8,7 @@ import org.hibernate.annotations.BatchSize; -import com.wayble.server.direction.entity.DirectionType; +import com.wayble.server.direction.entity.type.DirectionType; @Entity @Getter diff --git a/src/main/java/com/wayble/server/direction/entity/transportation/Route.java b/src/main/java/com/wayble/server/direction/entity/transportation/Route.java index d35c171..42dfce4 100644 --- a/src/main/java/com/wayble/server/direction/entity/transportation/Route.java +++ b/src/main/java/com/wayble/server/direction/entity/transportation/Route.java @@ -1,7 +1,7 @@ package com.wayble.server.direction.entity.transportation; -import com.wayble.server.direction.entity.DirectionType; import com.wayble.server.direction.entity.transportation.*; +import com.wayble.server.direction.entity.type.DirectionType; import jakarta.persistence.*; import lombok.*; diff --git a/src/main/java/com/wayble/server/direction/entity/DirectionType.java b/src/main/java/com/wayble/server/direction/entity/type/DirectionType.java similarity index 69% rename from src/main/java/com/wayble/server/direction/entity/DirectionType.java rename to src/main/java/com/wayble/server/direction/entity/type/DirectionType.java index 313c17d..d4b8015 100644 --- a/src/main/java/com/wayble/server/direction/entity/DirectionType.java +++ b/src/main/java/com/wayble/server/direction/entity/type/DirectionType.java @@ -1,4 +1,4 @@ -package com.wayble.server.direction.entity; +package com.wayble.server.direction.entity.type; public enum DirectionType { BUS, SUBWAY, WALK, diff --git a/src/main/java/com/wayble/server/direction/service/TransportationService.java b/src/main/java/com/wayble/server/direction/service/TransportationService.java index 70e4575..c0d9ac3 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -4,10 +4,10 @@ import com.wayble.server.direction.dto.TransportationGraphDto; import com.wayble.server.direction.dto.request.TransportationRequestDto; import com.wayble.server.direction.dto.response.TransportationResponseDto; -import com.wayble.server.direction.entity.DirectionType; import com.wayble.server.direction.entity.transportation.Edge; import com.wayble.server.direction.entity.transportation.Node; import com.wayble.server.direction.entity.transportation.Route; +import com.wayble.server.direction.entity.type.DirectionType; import com.wayble.server.direction.repository.EdgeRepository; import com.wayble.server.direction.repository.NodeRepository; import lombok.RequiredArgsConstructor; @@ -689,7 +689,7 @@ private List mergeConsecutiveRoutes(List p } else if (currentType == DirectionType.SUBWAY) { try { if (currentEdge.getStartNode() != null) { - TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId(), currentEdge.getRoute().getRouteId()); + TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId(), currentEdge.getRoute().getRouteId(), null); subwayInfo = new TransportationResponseDto.SubwayInfo( nodeInfo.wheelchair(), From 06d3484b74af2905a91fdab43e7be2f3b1d2adce Mon Sep 17 00:00:00 2001 From: hyoin Date: Tue, 19 Aug 2025 01:39:59 +0900 Subject: [PATCH 04/30] =?UTF-8?q?[feat]=20=EC=97=98=EB=A6=AC=EB=B2=A0?= =?UTF-8?q?=EC=9D=B4=ED=84=B0=20=EC=A0=95=EB=B3=B4=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../entity/transportation/Elevator.java | 7 ++-- .../repository/ElevatorRepository.java | 15 ++++++++ .../direction/service/FacilityService.java | 35 +++++++++++++++++-- 3 files changed, 49 insertions(+), 8 deletions(-) create mode 100644 src/main/java/com/wayble/server/direction/repository/ElevatorRepository.java diff --git a/src/main/java/com/wayble/server/direction/entity/transportation/Elevator.java b/src/main/java/com/wayble/server/direction/entity/transportation/Elevator.java index db0b6e1..ba6eca8 100644 --- a/src/main/java/com/wayble/server/direction/entity/transportation/Elevator.java +++ b/src/main/java/com/wayble/server/direction/entity/transportation/Elevator.java @@ -14,11 +14,8 @@ public class Elevator { @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @Column(name = "latitude", columnDefinition = "DECIMAL(10,7)", nullable = false) - private Double latitude; - - @Column(name = "longitude", columnDefinition = "DECIMAL(10,7)", nullable = false) - private Double longitude; + @Column(name = "location", nullable = false) + private String location; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "facility_id", nullable = false) diff --git a/src/main/java/com/wayble/server/direction/repository/ElevatorRepository.java b/src/main/java/com/wayble/server/direction/repository/ElevatorRepository.java new file mode 100644 index 0000000..c659d62 --- /dev/null +++ b/src/main/java/com/wayble/server/direction/repository/ElevatorRepository.java @@ -0,0 +1,15 @@ +package com.wayble.server.direction.repository; + +import com.wayble.server.direction.entity.transportation.Elevator; +import com.wayble.server.direction.entity.transportation.Facility; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; + +import java.util.List; + +public interface ElevatorRepository extends JpaRepository { + @Query("SELECT e FROM Elevator e WHERE e.facility = :facility") + List findByFacility(@Param("facility") Facility facility); +} + diff --git a/src/main/java/com/wayble/server/direction/service/FacilityService.java b/src/main/java/com/wayble/server/direction/service/FacilityService.java index 25f9f00..7bb9eb5 100644 --- a/src/main/java/com/wayble/server/direction/service/FacilityService.java +++ b/src/main/java/com/wayble/server/direction/service/FacilityService.java @@ -4,10 +4,15 @@ import com.wayble.server.direction.entity.transportation.Facility; import com.wayble.server.direction.entity.transportation.Node; import com.wayble.server.direction.entity.transportation.Wheelchair; +import com.wayble.server.direction.entity.transportation.Elevator; + import com.wayble.server.direction.external.kric.dto.KricToiletRawItem; import com.wayble.server.direction.external.kric.dto.KricToiletRawResponse; + +import com.wayble.server.direction.repository.ElevatorRepository; import com.wayble.server.direction.repository.FacilityRepository; import com.wayble.server.direction.repository.NodeRepository; +import com.wayble.server.direction.repository.RouteRepository; import com.wayble.server.direction.repository.WheelchairInfoRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; @@ -28,6 +33,7 @@ @Slf4j @RequiredArgsConstructor public class FacilityService { + private final ElevatorRepository elevatorRepository; private final FacilityRepository facilityRepository; private final NodeRepository nodeRepository; private final WheelchairInfoRepository wheelchairInfoRepository; @@ -54,8 +60,6 @@ public TransportationResponseDto.NodeInfo getNodeInfo(Long nodeId, Long routeId) } } - elevator = new ArrayList<>(); - Facility facility = facilityRepository.findByNodeId(nodeId).orElse(null); if (facility != null) { String stinCd = facility.getStinCd(); @@ -65,6 +69,8 @@ public TransportationResponseDto.NodeInfo getNodeInfo(Long nodeId, Long routeId) if (stinCd != null && railOprLsttCd != null && lnCd != null) { Map toiletInfo = getToiletInfo(facility); accessibleRestroom = toiletInfo.getOrDefault(stinCd, false); + + elevator = getElevatorInfo(facility, routeId); } else { log.error("Facility 정보 누락 - nodeId: {}, stinCd: {}, railOprLsttCd: {}, lnCd: {}", nodeId, stinCd, railOprLsttCd, lnCd); @@ -132,4 +138,27 @@ private Map getToiletInfo(Facility facility) { return stationToiletMap; } -} + + private List getElevatorInfo(Facility facility, Long routeId) { + List elevatorLocations = new ArrayList<>(); + + try { + List elevators = elevatorRepository.findByFacility(facility); + + for (Elevator elevator : elevators) { + String location = elevator.getLocation(); + if (location != null && !location.trim().isEmpty()) { + elevatorLocations.add(location.trim()); + } + } + + } catch(Exception e) { + log.error("엘리베이터 정보 조회 실패 - facilityId: {}, error: {}", + facility.getId(), e.getMessage(), e); + } + + return elevatorLocations; + } + + +} \ No newline at end of file From a2a98e288bcb2cb79afb1eeccda0669fef0ce9a7 Mon Sep 17 00:00:00 2001 From: hyoin Date: Tue, 19 Aug 2025 01:47:26 +0900 Subject: [PATCH 05/30] =?UTF-8?q?[fix]=20=EC=B6=9C=EA=B5=AC=20=EB=B2=88?= =?UTF-8?q?=ED=98=B8=20=EC=B6=9C=EB=A0=A5=EC=8B=9C=20=EC=A0=95=EB=A0=AC=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wayble/server/direction/service/FacilityService.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/main/java/com/wayble/server/direction/service/FacilityService.java b/src/main/java/com/wayble/server/direction/service/FacilityService.java index 7bb9eb5..e00d665 100644 --- a/src/main/java/com/wayble/server/direction/service/FacilityService.java +++ b/src/main/java/com/wayble/server/direction/service/FacilityService.java @@ -152,6 +152,8 @@ private List getElevatorInfo(Facility facility, Long routeId) { } } + elevatorLocations.sort(String::compareTo); + } catch(Exception e) { log.error("엘리베이터 정보 조회 실패 - facilityId: {}, error: {}", facility.getId(), e.getMessage(), e); From 0d51d0a2b5c93e82702d903f8bfc30dc79ee656e Mon Sep 17 00:00:00 2001 From: hyoin Date: Tue, 19 Aug 2025 01:47:36 +0900 Subject: [PATCH 06/30] =?UTF-8?q?[fix]=20=ED=95=A8=EC=88=98=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=88=98=EC=A0=96=E3=85=87?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wayble/server/direction/service/TransportationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/direction/service/TransportationService.java b/src/main/java/com/wayble/server/direction/service/TransportationService.java index c0d9ac3..0314bb9 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -689,7 +689,7 @@ private List mergeConsecutiveRoutes(List p } else if (currentType == DirectionType.SUBWAY) { try { if (currentEdge.getStartNode() != null) { - TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId(), currentEdge.getRoute().getRouteId(), null); + TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId(), currentEdge.getRoute().getRouteId()); subwayInfo = new TransportationResponseDto.SubwayInfo( nodeInfo.wheelchair(), From d897d30d36a5bafbbf2c3a12233cb18b55e848aa Mon Sep 17 00:00:00 2001 From: hyoin Date: Tue, 19 Aug 2025 02:22:08 +0900 Subject: [PATCH 07/30] =?UTF-8?q?Revert=20"[fix]=20=ED=95=A8=EC=88=98=20?= =?UTF-8?q?=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=88=98=EC=A0=96?= =?UTF-8?q?=E3=85=87"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 0d51d0a2b5c93e82702d903f8bfc30dc79ee656e. --- .../wayble/server/direction/service/TransportationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/direction/service/TransportationService.java b/src/main/java/com/wayble/server/direction/service/TransportationService.java index 0314bb9..c0d9ac3 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -689,7 +689,7 @@ private List mergeConsecutiveRoutes(List p } else if (currentType == DirectionType.SUBWAY) { try { if (currentEdge.getStartNode() != null) { - TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId(), currentEdge.getRoute().getRouteId()); + TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId(), currentEdge.getRoute().getRouteId(), null); subwayInfo = new TransportationResponseDto.SubwayInfo( nodeInfo.wheelchair(), From d8dc03cbc792aaf60d96bd60b76f54b8fda83821 Mon Sep 17 00:00:00 2001 From: hyoin Date: Tue, 19 Aug 2025 02:22:31 +0900 Subject: [PATCH 08/30] =?UTF-8?q?[fix]=20=ED=95=A8=EC=88=98=20=ED=8C=8C?= =?UTF-8?q?=EB=9D=BC=EB=AF=B8=ED=84=B0=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wayble/server/direction/service/TransportationService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/direction/service/TransportationService.java b/src/main/java/com/wayble/server/direction/service/TransportationService.java index c0d9ac3..0314bb9 100644 --- a/src/main/java/com/wayble/server/direction/service/TransportationService.java +++ b/src/main/java/com/wayble/server/direction/service/TransportationService.java @@ -689,7 +689,7 @@ private List mergeConsecutiveRoutes(List p } else if (currentType == DirectionType.SUBWAY) { try { if (currentEdge.getStartNode() != null) { - TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId(), currentEdge.getRoute().getRouteId(), null); + TransportationResponseDto.NodeInfo nodeInfo = facilityService.getNodeInfo(currentEdge.getStartNode().getId(), currentEdge.getRoute().getRouteId()); subwayInfo = new TransportationResponseDto.SubwayInfo( nodeInfo.wheelchair(), From ce61e362bb69a57a0f6c6f641fe5e8b74809d44c Mon Sep 17 00:00:00 2001 From: seungin Date: Tue, 19 Aug 2025 10:23:02 +0900 Subject: [PATCH 09/30] =?UTF-8?q?[refactor]=20=EB=A6=AC=EC=A1=B8=EB=B2=84?= =?UTF-8?q?=EC=97=90=EC=84=9C=20=ED=86=A0=ED=81=B0=EC=9C=BC=EB=A1=9C=20?= =?UTF-8?q?=EA=BA=BC=EB=82=B4=EB=8F=84=EB=A1=9D=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../resolver/CurrentUserArgumentResolver.java | 50 ++++++++++++------- 1 file changed, 32 insertions(+), 18 deletions(-) diff --git a/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java b/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java index 091bad7..72f2727 100644 --- a/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java +++ b/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java @@ -1,17 +1,24 @@ package com.wayble.server.auth.resolver; +import com.wayble.server.common.config.security.jwt.JwtTokenProvider; +import jakarta.servlet.http.HttpServletRequest; +import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.stereotype.Component; +import org.springframework.util.StringUtils; import org.springframework.web.bind.support.WebDataBinderFactory; import org.springframework.web.context.request.NativeWebRequest; import org.springframework.web.method.support.HandlerMethodArgumentResolver; import org.springframework.web.method.support.ModelAndViewContainer; @Component +@RequiredArgsConstructor public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver { + private final JwtTokenProvider jwtTokenProvider; + @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(CurrentUser.class) @@ -19,27 +26,34 @@ public boolean supportsParameter(MethodParameter parameter) { } @Override - public Object resolveArgument(MethodParameter parameter, - ModelAndViewContainer mav, - NativeWebRequest webRequest, - WebDataBinderFactory binderFactory) { + public Object resolveArgument( + MethodParameter parameter, + ModelAndViewContainer mav, + NativeWebRequest webRequest, + WebDataBinderFactory binderFactory + ) { Authentication auth = SecurityContextHolder.getContext().getAuthentication(); - if (auth == null) { - throw new IllegalStateException("인증 정보가 없습니다."); - } - - Object principal = auth.getPrincipal(); - if (principal instanceof Long l) return l; - if (principal instanceof Integer i) return i.longValue(); - if (principal instanceof String s) { - try { + if (auth != null) { + Object principal = auth.getPrincipal(); + if (principal instanceof Long l) { return l; } + if (principal instanceof Integer i) { return i.longValue(); } + if (principal instanceof String s && s.chars().allMatch(Character::isDigit)) { return Long.parseLong(s); - } catch (NumberFormatException ignored) {} + } + String name = auth.getName(); + if (name != null && name.chars().allMatch(Character::isDigit)) { + return Long.parseLong(name); + } } - try { - return Long.parseLong(auth.getName()); - } catch (Exception e) { - throw new IllegalStateException("userId를 추출할 수 없습니다.", e); + + HttpServletRequest request = webRequest.getNativeRequest(HttpServletRequest.class); + String authz = request != null ? request.getHeader("Authorization") : null; + if (StringUtils.hasText(authz) && authz.startsWith("Bearer ")) { + String token = authz.substring(7); + Long userId = jwtTokenProvider.getUserId(token); + if (userId != null) { return userId; } } + + throw new IllegalStateException("인증 정보가 없거나 userId를 추출할 수 없습니다."); } } \ No newline at end of file From b44069f278f120a25d4afadc9c19d82bd1a9a6e2 Mon Sep 17 00:00:00 2001 From: seungin Date: Tue, 19 Aug 2025 10:40:52 +0900 Subject: [PATCH 10/30] =?UTF-8?q?[refactor]=20swagger=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=95=84=EC=88=98=20=ED=8C=8C=EB=9D=BC=EB=AF=B8=ED=84=B0=20use?= =?UTF-8?q?rId=20=EC=9E=85=EB=A0=A5=20=EC=82=AD=EC=A0=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/review/controller/ReviewController.java | 4 ++-- .../server/user/controller/UserPlaceController.java | 9 +++++---- 2 files changed, 7 insertions(+), 6 deletions(-) diff --git a/src/main/java/com/wayble/server/review/controller/ReviewController.java b/src/main/java/com/wayble/server/review/controller/ReviewController.java index 1661a7c..840b780 100644 --- a/src/main/java/com/wayble/server/review/controller/ReviewController.java +++ b/src/main/java/com/wayble/server/review/controller/ReviewController.java @@ -13,7 +13,7 @@ import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; import org.springframework.web.bind.annotation.*; - +import io.swagger.v3.oas.annotations.Parameter; import java.util.List; @@ -37,7 +37,7 @@ public class ReviewController { }) public CommonResponse registerReview( @PathVariable Long waybleZoneId, - @CurrentUser Long userId, + @Parameter(hidden = true) @CurrentUser Long userId, @RequestBody @Valid ReviewRegisterDto dto ) { reviewService.registerReview(waybleZoneId, userId, dto); 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 5dc568a..8401614 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -9,6 +9,7 @@ import com.wayble.server.user.service.UserPlaceService; import com.wayble.server.wayblezone.dto.WaybleZoneListResponseDto; import io.swagger.v3.oas.annotations.Operation; +import io.swagger.v3.oas.annotations.Parameter; import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; @@ -34,7 +35,7 @@ public class UserPlaceController { @ApiResponse(responseCode = "403", description = "권한이 없습니다.") }) public CommonResponse saveUserPlace( - @CurrentUser Long userId, + @Parameter(hidden = true) @CurrentUser Long userId, @RequestBody @Valid UserPlaceRequestDto request ) { userPlaceService.saveUserPlace(userId, request); // userId 파라미터로 넘김 @@ -49,7 +50,7 @@ public CommonResponse saveUserPlace( @ApiResponse(responseCode = "403", description = "권한이 없습니다.") }) public CommonResponse> getMyPlaceSummaries( - @CurrentUser Long userId, + @Parameter(hidden = true) @CurrentUser Long userId, @RequestParam(name = "sort", defaultValue = "latest") String sort ) { List summaries = userPlaceService.getMyPlaceSummaries(userId, sort); @@ -66,7 +67,7 @@ public CommonResponse> getMyPlaceSummaries( @ApiResponse(responseCode = "403", description = "권한이 없습니다.") }) public CommonResponse> getZonesInPlace( - @CurrentUser Long userId, + @Parameter(hidden = true) @CurrentUser Long userId, @RequestParam Long placeId, @RequestParam(defaultValue = "1") Integer page, @RequestParam(defaultValue = "20") Integer size @@ -86,7 +87,7 @@ public CommonResponse> getZonesInPlace( @ApiResponse(responseCode = "403", description = "권한이 없습니다.") }) public CommonResponse removeZoneFromPlace( - @CurrentUser Long userId, + @Parameter(hidden = true) @CurrentUser Long userId, @RequestBody @Valid UserPlaceRemoveRequestDto request ) { userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId()); From 47b3255d3e302ef59e842ff45833302de4234573 Mon Sep 17 00:00:00 2001 From: seungin Date: Tue, 19 Aug 2025 11:08:57 +0900 Subject: [PATCH 11/30] =?UTF-8?q?[feat]=20Auth=20(=EB=A6=AC=EC=A1=B8?= =?UTF-8?q?=EB=B2=84)=20=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=EC=BC=80?= =?UTF-8?q?=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/auth/exception/AuthErrorCase.java | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java diff --git a/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java b/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java new file mode 100644 index 0000000..ae589b4 --- /dev/null +++ b/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java @@ -0,0 +1,15 @@ +package com.wayble.server.auth.exception; + +import com.wayble.server.common.exception.ErrorCase; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +@Getter +@RequiredArgsConstructor +public enum AuthErrorCase implements ErrorCase { + UNAUTHORIZED(401, 1001, "인증 정보가 없거나 userId를 추출할 수 없습니다."); + + private final Integer httpStatusCode; + private final Integer errorCode; + private final String message; +} From 3faefa3c0f03e3c3a3fffd8cf1f2797cf818a070 Mon Sep 17 00:00:00 2001 From: seungin Date: Tue, 19 Aug 2025 11:10:45 +0900 Subject: [PATCH 12/30] =?UTF-8?q?[refactor]=20=EB=A6=AC=EC=A1=B8=EB=B2=84?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=98=88=EC=99=B8=EC=B2=98=EB=A6=AC=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 --- .../server/auth/resolver/CurrentUserArgumentResolver.java | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java b/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java index 72f2727..f38d5bc 100644 --- a/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java +++ b/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java @@ -1,6 +1,8 @@ package com.wayble.server.auth.resolver; +import com.wayble.server.auth.exception.AuthErrorCase; import com.wayble.server.common.config.security.jwt.JwtTokenProvider; +import com.wayble.server.common.exception.ApplicationException; import jakarta.servlet.http.HttpServletRequest; import lombok.RequiredArgsConstructor; import org.springframework.core.MethodParameter; @@ -54,6 +56,6 @@ public Object resolveArgument( if (userId != null) { return userId; } } - throw new IllegalStateException("인증 정보가 없거나 userId를 추출할 수 없습니다."); + throw new ApplicationException(AuthErrorCase.UNAUTHORIZED); } } \ No newline at end of file From 67a5faadbe9481ca01bb7b36fcdbf248a9bc916e Mon Sep 17 00:00:00 2001 From: seungin Date: Tue, 19 Aug 2025 11:22:16 +0900 Subject: [PATCH 13/30] =?UTF-8?q?[refactor]=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/auth/resolver/CurrentUserArgumentResolver.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java b/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java index f38d5bc..cdb7e40 100644 --- a/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java +++ b/src/main/java/com/wayble/server/auth/resolver/CurrentUserArgumentResolver.java @@ -52,8 +52,12 @@ public Object resolveArgument( String authz = request != null ? request.getHeader("Authorization") : null; if (StringUtils.hasText(authz) && authz.startsWith("Bearer ")) { String token = authz.substring(7); - Long userId = jwtTokenProvider.getUserId(token); - if (userId != null) { return userId; } + try { + Long userId = jwtTokenProvider.getUserId(token); + if (userId != null) { return userId; } + } catch (IllegalArgumentException e) { + throw new ApplicationException(AuthErrorCase.UNAUTHORIZED); + } } throw new ApplicationException(AuthErrorCase.UNAUTHORIZED); From 8503d61dc04fe81cf3d2b935d9144dd076d26f3c Mon Sep 17 00:00:00 2001 From: seungin Date: Tue, 19 Aug 2025 11:38:39 +0900 Subject: [PATCH 14/30] =?UTF-8?q?[refactor]=20Auth=20errorCode=20=EC=88=AB?= =?UTF-8?q?=EC=9E=90=20=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/auth/exception/AuthErrorCase.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java b/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java index ae589b4..ad552a3 100644 --- a/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java +++ b/src/main/java/com/wayble/server/auth/exception/AuthErrorCase.java @@ -7,7 +7,7 @@ @Getter @RequiredArgsConstructor public enum AuthErrorCase implements ErrorCase { - UNAUTHORIZED(401, 1001, "인증 정보가 없거나 userId를 추출할 수 없습니다."); + UNAUTHORIZED(401, 7001, "인증 정보가 없거나 userId를 추출할 수 없습니다."); private final Integer httpStatusCode; private final Integer errorCode; From 3e37fa96765b37cb00c3783a5045aa3d65889757 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 01:53:52 +0900 Subject: [PATCH 15/30] =?UTF-8?q?[feat]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=A0=80=EC=9E=A5=ED=95=A0=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1=20=EA=B4=80=EB=A0=A8=20=EC=9A=94?= =?UTF-8?q?=EC=B2=AD=20Dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../wayble/server/user/dto/UserPlaceCreateRequestDto.java | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java new file mode 100644 index 0000000..816c20d --- /dev/null +++ b/src/main/java/com/wayble/server/user/dto/UserPlaceCreateRequestDto.java @@ -0,0 +1,8 @@ +package com.wayble.server.user.dto; + +import jakarta.validation.constraints.NotBlank; + +public record UserPlaceCreateRequestDto( + @NotBlank(message = "제목은 필수입니다.") String title, + String color +) {} From 6bcd32bac6af6330a1eec86ab73f11235d18e8f7 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 01:54:50 +0900 Subject: [PATCH 16/30] =?UTF-8?q?[feat]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=A0=80=EC=9E=A5=ED=95=A0=20=EB=A6=AC=EC=8A=A4?= =?UTF-8?q?=ED=8A=B8=20=EC=83=9D=EC=84=B1=20=EA=B4=80=EB=A0=A8=20=EC=9D=91?= =?UTF-8?q?=EB=8B=B5=20Dto=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/user/dto/UserPlaceCreateResponseDto.java | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java new file mode 100644 index 0000000..a8be930 --- /dev/null +++ b/src/main/java/com/wayble/server/user/dto/UserPlaceCreateResponseDto.java @@ -0,0 +1,11 @@ +package com.wayble.server.user.dto; + +import lombok.Builder; + +@Builder +public record UserPlaceCreateResponseDto( + Long placeId, + String title, + String color, + String message +) {} From a9b7c9a7d50cff004b946d7463dd7c3c3f553463 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 01:56:03 +0900 Subject: [PATCH 17/30] =?UTF-8?q?[feat]=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EC=97=90=20=EC=9B=A8=EC=9D=B4=EB=B8=94=EC=A1=B4=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80=20=EC=9A=94=EC=B2=AD=20DTO=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/user/dto/UserPlaceAddZonesRequestDto.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java new file mode 100644 index 0000000..34d62e3 --- /dev/null +++ b/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java @@ -0,0 +1,10 @@ +package com.wayble.server.user.dto; + +import jakarta.validation.constraints.NotEmpty; +import jakarta.validation.constraints.NotNull; +import java.util.List; + +public record UserPlaceAddZonesRequestDto( + @NotNull Long placeId, + @NotEmpty List waybleZoneId +) {} From aec33b8be8e34d7c2bdea20ec4b62d278f012661 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 01:57:10 +0900 Subject: [PATCH 18/30] =?UTF-8?q?[refactor]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=B6=94=EA=B0=80=20=EB=A6=AC=EC=8A=A4=ED=8A=B8=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=20=EC=BC=80=EC=9D=B4?= =?UTF-8?q?=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/com/wayble/server/user/exception/UserErrorCase.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/user/exception/UserErrorCase.java b/src/main/java/com/wayble/server/user/exception/UserErrorCase.java index 031808b..1d28320 100644 --- a/src/main/java/com/wayble/server/user/exception/UserErrorCase.java +++ b/src/main/java/com/wayble/server/user/exception/UserErrorCase.java @@ -22,7 +22,8 @@ public enum UserErrorCase implements ErrorCase { NICKNAME_REQUIRED(400, 1012,"nickname 파라미터는 필수입니다."), NICKNAME_DUPLICATED(409,1013, "이미 사용 중인 닉네임입니다."), PLACE_NOT_FOUND(404, 1014, "저장된 장소를 찾을 수 없습니다."), - PLACE_MAPPING_NOT_FOUND(404, 1015, "해당 장소에 해당 웨이블존이 없습니다."); + PLACE_MAPPING_NOT_FOUND(404, 1015, "해당 장소에 해당 웨이블존이 없습니다."), + PLACE_TITLE_DUPLICATED(400, 1016, "동일한 이름의 리스트가 이미 있습니다."); private final Integer httpStatusCode; private final Integer errorCode; From c288b5bddb590d5540d5d2784f84becee1692737 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 02:09:40 +0900 Subject: [PATCH 19/30] =?UTF-8?q?[refactor]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=A0=80=EC=9E=A5=20=EA=B4=80=EB=A0=A8=20=EC=84=9C?= =?UTF-8?q?=EB=B9=84=EC=8A=A4=20=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/user/service/UserPlaceService.java | 75 ++++++++++++------- 1 file changed, 46 insertions(+), 29 deletions(-) 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 194f852..c5292a8 100644 --- a/src/main/java/com/wayble/server/user/service/UserPlaceService.java +++ b/src/main/java/com/wayble/server/user/service/UserPlaceService.java @@ -2,6 +2,7 @@ import com.wayble.server.common.exception.ApplicationException; +import com.wayble.server.user.dto.UserPlaceCreateRequestDto; import com.wayble.server.user.dto.UserPlaceRequestDto; import com.wayble.server.user.dto.UserPlaceSummaryDto; import com.wayble.server.user.entity.User; @@ -22,7 +23,9 @@ import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Set; @Service @RequiredArgsConstructor @@ -34,43 +37,57 @@ public class UserPlaceService { private final UserPlaceWaybleZoneMappingRepository mappingRepository; @Transactional - public void saveUserPlace(Long userId, UserPlaceRequestDto request) { - // 유저 존재 확인 + public Long createPlaceList(Long userId, UserPlaceCreateRequestDto request) { User user = userRepository.findById(userId) .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); - // 웨이블존 존재 확인 - WaybleZone waybleZone = waybleZoneRepository.findById(request.waybleZoneId()) - .orElseThrow(() -> new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND)); + userPlaceRepository.findByUser_IdAndTitle(userId, request.title()) + .ifPresent(p -> { throw new ApplicationException(UserErrorCase.PLACE_TITLE_DUPLICATED); }); - // 중복 저장 확인 - boolean duplicated = mappingRepository.existsByUserPlace_User_IdAndWaybleZone_Id(userId, request.waybleZoneId()); - if (duplicated) { - throw new ApplicationException(UserErrorCase.PLACE_ALREADY_SAVED); + String color = (request.color() == null || request.color().isBlank()) ? "GRAY" : request.color(); + + UserPlace saved = userPlaceRepository.save( + UserPlace.builder() + .title(request.title()) + .color(color) + .user(user) + .savedCount(0) + .build() + ); + return saved.getId(); + } + + @Transactional + public void addZonesToPlace(Long userId, Long placeId, List waybleZoneIds) { + UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND)); + + Set uniqueIds = new LinkedHashSet<>(waybleZoneIds); + + int added = 0; + for (Long zoneId : uniqueIds) { + WaybleZone zone = waybleZoneRepository.findById(zoneId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND)); + + boolean exists = mappingRepository.existsByUserPlace_IdAndWaybleZone_Id(placeId, zoneId); + if (exists) continue; + + mappingRepository.save(UserPlaceWaybleZoneMapping.builder() + .userPlace(place) + .waybleZone(zone) + .build()); + + place.increaseCount(); + zone.addLikes(1); + waybleZoneRepository.save(zone); + + added++; } - String color = (request.color() == null || request.color().isBlank()) ? "GRAY" : request.color(); - UserPlace userPlace = userPlaceRepository.findByUser_IdAndTitle(userId, request.title()) - .orElseGet(() -> userPlaceRepository.save( - UserPlace.builder() - .title(request.title()) - .color(color) - .user(user) - .build() - )); - - mappingRepository.save(UserPlaceWaybleZoneMapping.builder() - .userPlace(userPlace) - .waybleZone(waybleZone) - .build()); - - userPlace.increaseCount(); - userPlaceRepository.save(userPlace); - - waybleZone.addLikes(1); - waybleZoneRepository.save(waybleZone); + if (added > 0) userPlaceRepository.save(place); } + @Transactional(readOnly = true) public List getMyPlaceSummaries(Long userId, String sort) { userRepository.findById(userId) From 2fbb794a49f179c1f05e57f7ef517d20fccb718d Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 02:10:16 +0900 Subject: [PATCH 20/30] =?UTF-8?q?[refactor]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=A0=80=EC=9E=A5=20=EA=B4=80=EB=A0=A8=20Dto=20?= =?UTF-8?q?=ED=95=84=EB=93=9C=EB=AA=85=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java index 34d62e3..bd1524c 100644 --- a/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java +++ b/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java @@ -6,5 +6,5 @@ public record UserPlaceAddZonesRequestDto( @NotNull Long placeId, - @NotEmpty List waybleZoneId + @NotEmpty List waybleZoneIds ) {} From 9700c53d87db110aeaafaa8d619e9ada40d172ab Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 02:15:05 +0900 Subject: [PATCH 21/30] =?UTF-8?q?[fix]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=A0=80=EC=9E=A5=20=EA=B4=80=EB=A0=A8=20=EC=BB=A8?= =?UTF-8?q?=ED=8A=B8=EB=A1=A4=EB=9F=AC=20=EB=A1=9C=EC=A7=81=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserPlaceController.java | 78 ++++++++++++------- 1 file changed, 48 insertions(+), 30 deletions(-) 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 8401614..f949312 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -3,9 +3,7 @@ import com.wayble.server.auth.resolver.CurrentUser; import com.wayble.server.common.response.CommonResponse; -import com.wayble.server.user.dto.UserPlaceRemoveRequestDto; -import com.wayble.server.user.dto.UserPlaceRequestDto; -import com.wayble.server.user.dto.UserPlaceSummaryDto; +import com.wayble.server.user.dto.*; import com.wayble.server.user.service.UserPlaceService; import com.wayble.server.wayblezone.dto.WaybleZoneListResponseDto; import io.swagger.v3.oas.annotations.Operation; @@ -27,19 +25,25 @@ public class UserPlaceController { private final UserPlaceService userPlaceService; @PostMapping - @Operation(summary = "유저 장소 저장", description = "유저가 웨이블존을 장소로 저장합니다.") + @Operation(summary = "웨이블존 저장할 리스트 생성", + description = "제목과 색상을 받아 웨이블존을 저장할 리스트를 생성합니다.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "장소 저장 성공"), - @ApiResponse(responseCode = "400", description = "이미 저장한 장소입니다."), - @ApiResponse(responseCode = "404", description = "해당 유저 또는 웨이블존이 존재하지 않음"), - @ApiResponse(responseCode = "403", description = "권한이 없습니다.") + @ApiResponse(responseCode = "200", description = "리스트 생성 성공"), + @ApiResponse(responseCode = "400", description = "동일한 리스트명이 이미 존재") }) - public CommonResponse saveUserPlace( + public CommonResponse createPlaceList( @Parameter(hidden = true) @CurrentUser Long userId, - @RequestBody @Valid UserPlaceRequestDto request + @RequestBody @Valid UserPlaceCreateRequestDto request ) { - userPlaceService.saveUserPlace(userId, request); // userId 파라미터로 넘김 - return CommonResponse.success("장소가 저장되었습니다."); + Long placeId = userPlaceService.createPlaceList(userId, request); + return CommonResponse.success( + UserPlaceCreateResponseDto.builder() + .placeId(placeId) + .title(request.title()) + .color((request.color() == null || request.color().isBlank()) ? "GRAY" : request.color()) + .message("리스트가 생성되었습니다.") + .build() + ); } @GetMapping @@ -57,6 +61,38 @@ public CommonResponse> getMyPlaceSummaries( return CommonResponse.success(summaries); } + @DeleteMapping + @Operation( + summary = "장소에서 웨이블존 제거", + description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "제거 성공"), + @ApiResponse(responseCode = "404", description = "장소 또는 매핑 정보를 찾을 수 없음"), + @ApiResponse(responseCode = "403", description = "권한이 없습니다.") + }) + public CommonResponse removeZoneFromPlace( + @Parameter(hidden = true) @CurrentUser Long userId, + @RequestBody @Valid UserPlaceRemoveRequestDto request + ) { + userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId()); + return CommonResponse.success("제거되었습니다."); + } + + @PostMapping("/zones") + @Operation(summary = "리스트에 웨이블존 추가", + description = "placeId와 waybleZoneId 배열을 받아 여러 웨이블존을 리스트에 추가합니다.") + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "웨이블존 추가 성공"), + @ApiResponse(responseCode = "404", description = "유저/리스트/웨이블존을 찾을 수 없음") + }) + public CommonResponse addZonesToPlace( + @Parameter(hidden = true) @CurrentUser Long userId, + @RequestBody @Valid UserPlaceAddZonesRequestDto request + ) { + userPlaceService.addZonesToPlace(userId, request.placeId(), request.waybleZoneIds()); + return CommonResponse.success("리스트에 웨이블존이 추가되었습니다."); + } @GetMapping("/zones") @Operation(summary = "특정 장소 내 웨이블존 목록 조회(페이징)", @@ -75,22 +111,4 @@ public CommonResponse> getZonesInPlace( Page zones = userPlaceService.getZonesInPlace(userId, placeId, page, size); return CommonResponse.success(zones); } - - @DeleteMapping - @Operation( - summary = "장소에서 웨이블존 제거", - description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다." - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "제거 성공"), - @ApiResponse(responseCode = "404", description = "장소 또는 매핑 정보를 찾을 수 없음"), - @ApiResponse(responseCode = "403", description = "권한이 없습니다.") - }) - public CommonResponse removeZoneFromPlace( - @Parameter(hidden = true) @CurrentUser Long userId, - @RequestBody @Valid UserPlaceRemoveRequestDto request - ) { - userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId()); - return CommonResponse.success("제거되었습니다."); - } } From 4798b2715f8b721f673b331628c0b039415b2947 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 03:33:15 +0900 Subject: [PATCH 22/30] =?UTF-8?q?[fix]=20=EC=98=A4=ED=94=84=EB=B0=94?= =?UTF-8?q?=EC=9D=B4=EC=9B=90=20=EB=B2=84=EA=B7=B8=20=EA=B4=80=EB=A0=A8=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/user/controller/UserPlaceController.java | 8 +++++--- .../com/wayble/server/user/service/UserPlaceService.java | 4 +--- 2 files changed, 6 insertions(+), 6 deletions(-) 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 f949312..03984c2 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -11,6 +11,8 @@ import io.swagger.v3.oas.annotations.responses.ApiResponse; import io.swagger.v3.oas.annotations.responses.ApiResponses; import jakarta.validation.Valid; +import jakarta.validation.constraints.Max; +import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; import org.springframework.web.bind.annotation.*; @@ -96,7 +98,7 @@ public CommonResponse addZonesToPlace( @GetMapping("/zones") @Operation(summary = "특정 장소 내 웨이블존 목록 조회(페이징)", - description = "placeId로 해당 장소 내부의 웨이블존 카드 목록을 반환합니다. (page는 1부터 시작.)") + description = "placeId로 해당 장소 내부의 웨이블존 카드 목록을 반환합니다. (page는 0부터 시작.)") @ApiResponses({ @ApiResponse(responseCode = "200", description = "조회 성공"), @ApiResponse(responseCode = "404", description = "유저/장소를 찾을 수 없음"), @@ -105,8 +107,8 @@ public CommonResponse addZonesToPlace( public CommonResponse> getZonesInPlace( @Parameter(hidden = true) @CurrentUser Long userId, @RequestParam Long placeId, - @RequestParam(defaultValue = "1") Integer page, - @RequestParam(defaultValue = "20") Integer size + @RequestParam(defaultValue = "0") @Min(0) Integer page, + @RequestParam(defaultValue = "20") @Min(1) @Max(100) Integer size ) { Page zones = userPlaceService.getZonesInPlace(userId, placeId, page, size); return CommonResponse.success(zones); 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 c5292a8..059d5c6 100644 --- a/src/main/java/com/wayble/server/user/service/UserPlaceService.java +++ b/src/main/java/com/wayble/server/user/service/UserPlaceService.java @@ -112,9 +112,7 @@ public Page getZonesInPlace(Long userId, Long placeId UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, userId) .orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND)); - int zeroBased = Math.max(0, page - 1); - - Pageable pageable = PageRequest.of(zeroBased, size, Sort.by(Sort.Direction.DESC, "id")); + Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "id")); Page zones = mappingRepository.findZonesByPlaceId(place.getId(), pageable); return zones.map(z -> From e11cbca3e6145bd8d29cbc0f8a2dca3fcaa21e41 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 03:35:46 +0900 Subject: [PATCH 23/30] =?UTF-8?q?[refactor]=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wayble/server/user/service/UserPlaceService.java | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) 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 059d5c6..caa4d07 100644 --- a/src/main/java/com/wayble/server/user/service/UserPlaceService.java +++ b/src/main/java/com/wayble/server/user/service/UserPlaceService.java @@ -41,17 +41,18 @@ public Long createPlaceList(Long userId, UserPlaceCreateRequestDto request) { User user = userRepository.findById(userId) .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); - userPlaceRepository.findByUser_IdAndTitle(userId, request.title()) + String normalizedTitle = request.title().trim(); + userPlaceRepository.findByUser_IdAndTitle(userId, normalizedTitle) .ifPresent(p -> { throw new ApplicationException(UserErrorCase.PLACE_TITLE_DUPLICATED); }); - String color = (request.color() == null || request.color().isBlank()) ? "GRAY" : request.color(); + String color = request.color() == null ? null : request.color().trim(); + color = (color == null || color.isEmpty()) ? "GRAY" : color.toUpperCase(); UserPlace saved = userPlaceRepository.save( UserPlace.builder() - .title(request.title()) + .title(normalizedTitle) .color(color) .user(user) - .savedCount(0) .build() ); return saved.getId(); From fc63e8f8a5639fc4c09950ade3683ec6239bc541 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 03:52:15 +0900 Subject: [PATCH 24/30] =?UTF-8?q?[refactor]=20=ED=8E=98=EC=9D=B4=EC=A7=95?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=EC=97=90=20=EB=94=B0=EB=9D=BC=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=EB=8F=84=20=EC=A0=95=EA=B7=9C=ED=99=94?= =?UTF-8?q?=ED=95=9C=20=EA=B0=92=EC=9C=BC=EB=A1=9C=20=EC=82=AC=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../server/user/controller/UserPlaceController.java | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 03984c2..b0761a9 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -38,11 +38,15 @@ public CommonResponse createPlaceList( @RequestBody @Valid UserPlaceCreateRequestDto request ) { Long placeId = userPlaceService.createPlaceList(userId, request); + String normalizedTitle = request.title().trim(); + String normalizedColor = (request.color() == null || request.color().isBlank()) + ? "GRAY" + : request.color().trim().toUpperCase(); return CommonResponse.success( UserPlaceCreateResponseDto.builder() .placeId(placeId) - .title(request.title()) - .color((request.color() == null || request.color().isBlank()) ? "GRAY" : request.color()) + .title(normalizedTitle) + .color(normalizedColor) .message("리스트가 생성되었습니다.") .build() ); From 697aaa57547b7b885a1d9c6509335f71dc683b48 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 03:53:41 +0900 Subject: [PATCH 25/30] =?UTF-8?q?[refactor]=20UserPlaceController=EC=97=90?= =?UTF-8?q?=20@validated=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wayble/server/user/controller/UserPlaceController.java | 2 ++ 1 file changed, 2 insertions(+) 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 b0761a9..7c1388a 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -15,11 +15,13 @@ import jakarta.validation.constraints.Min; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Page; +import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import java.util.List; @RestController +@Validated @RequestMapping("/api/v1/users/places") @RequiredArgsConstructor public class UserPlaceController { From f725de5eeb1c7d609f8264651f43f1c0e896de02 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 10:28:47 +0900 Subject: [PATCH 26/30] =?UTF-8?q?[refactor]=20=EC=9B=A8=EC=9D=B4=EB=B8=94?= =?UTF-8?q?=EC=A1=B4=20=EC=A0=80=EC=9E=A5=20=EB=B0=8F=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20=EA=B4=80=EB=A0=A8=20swagger=20=EC=9D=B4=EB=A6=84=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 --- .../server/user/controller/UserPlaceController.java | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) 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 7c1388a..49a618b 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -55,7 +55,7 @@ public CommonResponse createPlaceList( } @GetMapping - @Operation(summary = "내 장소 리스트 요약 조회", description = "장소 관련 목록(리스트)만 반환합니다(개수 포함).") + @Operation(summary = "내가 저장한 리스트 요약 조회", description = "장소 관련 목록(리스트)만 반환합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "조회 성공"), @ApiResponse(responseCode = "404", description = "유저를 찾을 수 없음"), @@ -71,7 +71,7 @@ public CommonResponse> getMyPlaceSummaries( @DeleteMapping @Operation( - summary = "장소에서 웨이블존 제거", + summary = "내가 저장한 리스트에서 웨이블존 제거", description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다." ) @ApiResponses({ @@ -84,7 +84,7 @@ public CommonResponse removeZoneFromPlace( @RequestBody @Valid UserPlaceRemoveRequestDto request ) { userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId()); - return CommonResponse.success("제거되었습니다."); + return CommonResponse.success("성공적으로 제거되었습니다."); } @PostMapping("/zones") @@ -103,8 +103,8 @@ public CommonResponse addZonesToPlace( } @GetMapping("/zones") - @Operation(summary = "특정 장소 내 웨이블존 목록 조회(페이징)", - description = "placeId로 해당 장소 내부의 웨이블존 카드 목록을 반환합니다. (page는 0부터 시작.)") + @Operation(summary = "저장한 리스트 내 웨이블존 목록 조회(페이징)", + description = "placeId로 해당 장소 내부의 웨이블존 목록을 반환합니다. (page는 0부터 시작.)") @ApiResponses({ @ApiResponse(responseCode = "200", description = "조회 성공"), @ApiResponse(responseCode = "404", description = "유저/장소를 찾을 수 없음"), From b1df4ead54dc3fe0f671b3be44873fd024f8f781 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 11:30:33 +0900 Subject: [PATCH 27/30] =?UTF-8?q?[fix]=20=ED=95=98=EB=82=98=EC=9D=98=20?= =?UTF-8?q?=EC=9B=A8=EC=9D=B4=EB=B8=94=EC=A1=B4=EC=97=90=20=EC=97=AC?= =?UTF-8?q?=EB=9F=AC=EA=B0=9C=EC=9D=98=20=EB=A6=AC=EC=8A=A4=ED=8A=B8?= =?UTF-8?q?=EB=A5=BC=20=EC=A0=80=EC=9E=A5=ED=95=98=EB=8F=84=EB=A1=9D=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserPlaceController.java | 12 ++++---- .../user/dto/UserPlaceAddZonesRequestDto.java | 4 +-- .../server/user/dto/UserPlaceRequestDto.java | 9 ------ .../server/user/dto/UserResponseDto.java | 4 --- .../server/user/exception/UserErrorCase.java | 2 +- .../server/user/service/UserPlaceService.java | 30 +++++++++++-------- 6 files changed, 26 insertions(+), 35 deletions(-) delete mode 100644 src/main/java/com/wayble/server/user/dto/UserPlaceRequestDto.java delete mode 100644 src/main/java/com/wayble/server/user/dto/UserResponseDto.java 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 49a618b..a0a7151 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -69,7 +69,7 @@ public CommonResponse> getMyPlaceSummaries( return CommonResponse.success(summaries); } - @DeleteMapping + @DeleteMapping("/zones") @Operation( summary = "내가 저장한 리스트에서 웨이블존 제거", description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다." @@ -88,18 +88,18 @@ public CommonResponse removeZoneFromPlace( } @PostMapping("/zones") - @Operation(summary = "리스트에 웨이블존 추가", - description = "placeId와 waybleZoneId 배열을 받아 여러 웨이블존을 리스트에 추가합니다.") + @Operation(summary = "웨이블존에 저장한 리스트 추가 (여러개 가능)", + description = "웨이블존에 사용자가 요청한 리스트들을 추가합니다.") @ApiResponses({ - @ApiResponse(responseCode = "200", description = "웨이블존 추가 성공"), + @ApiResponse(responseCode = "200", description = "웨이블존에 리스트 추가 성공"), @ApiResponse(responseCode = "404", description = "유저/리스트/웨이블존을 찾을 수 없음") }) public CommonResponse addZonesToPlace( @Parameter(hidden = true) @CurrentUser Long userId, @RequestBody @Valid UserPlaceAddZonesRequestDto request ) { - userPlaceService.addZonesToPlace(userId, request.placeId(), request.waybleZoneIds()); - return CommonResponse.success("리스트에 웨이블존이 추가되었습니다."); + int added = userPlaceService.addZoneToPlaces(userId, request.placeIds(), request.waybleZoneId()); + return CommonResponse.success(String.format("%d개 리스트에 추가되었습니다.", added)); } @GetMapping("/zones") diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java index bd1524c..ae986b4 100644 --- a/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java +++ b/src/main/java/com/wayble/server/user/dto/UserPlaceAddZonesRequestDto.java @@ -5,6 +5,6 @@ import java.util.List; public record UserPlaceAddZonesRequestDto( - @NotNull Long placeId, - @NotEmpty List waybleZoneIds + @NotEmpty List<@NotNull Long> placeIds, + @NotNull Long waybleZoneId ) {} diff --git a/src/main/java/com/wayble/server/user/dto/UserPlaceRequestDto.java b/src/main/java/com/wayble/server/user/dto/UserPlaceRequestDto.java deleted file mode 100644 index a265e39..0000000 --- a/src/main/java/com/wayble/server/user/dto/UserPlaceRequestDto.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.wayble.server.user.dto; - -import jakarta.validation.constraints.NotNull; - -public record UserPlaceRequestDto( - @NotNull Long waybleZoneId, - @NotNull String title, - String color -) {} diff --git a/src/main/java/com/wayble/server/user/dto/UserResponseDto.java b/src/main/java/com/wayble/server/user/dto/UserResponseDto.java deleted file mode 100644 index 7e5a712..0000000 --- a/src/main/java/com/wayble/server/user/dto/UserResponseDto.java +++ /dev/null @@ -1,4 +0,0 @@ -package com.wayble.server.user.dto; - -public record UserResponseDto() { -} diff --git a/src/main/java/com/wayble/server/user/exception/UserErrorCase.java b/src/main/java/com/wayble/server/user/exception/UserErrorCase.java index 1d28320..4fb835a 100644 --- a/src/main/java/com/wayble/server/user/exception/UserErrorCase.java +++ b/src/main/java/com/wayble/server/user/exception/UserErrorCase.java @@ -23,7 +23,7 @@ public enum UserErrorCase implements ErrorCase { NICKNAME_DUPLICATED(409,1013, "이미 사용 중인 닉네임입니다."), PLACE_NOT_FOUND(404, 1014, "저장된 장소를 찾을 수 없습니다."), PLACE_MAPPING_NOT_FOUND(404, 1015, "해당 장소에 해당 웨이블존이 없습니다."), - PLACE_TITLE_DUPLICATED(400, 1016, "동일한 이름의 리스트가 이미 있습니다."); + PLACE_TITLE_DUPLICATED(409, 1016, "동일한 이름의 리스트가 이미 있습니다."); private final Integer httpStatusCode; private final Integer errorCode; 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 caa4d07..af048e7 100644 --- a/src/main/java/com/wayble/server/user/service/UserPlaceService.java +++ b/src/main/java/com/wayble/server/user/service/UserPlaceService.java @@ -3,7 +3,6 @@ import com.wayble.server.common.exception.ApplicationException; import com.wayble.server.user.dto.UserPlaceCreateRequestDto; -import com.wayble.server.user.dto.UserPlaceRequestDto; import com.wayble.server.user.dto.UserPlaceSummaryDto; import com.wayble.server.user.entity.User; import com.wayble.server.user.entity.UserPlace; @@ -59,18 +58,21 @@ public Long createPlaceList(Long userId, UserPlaceCreateRequestDto request) { } @Transactional - public void addZonesToPlace(Long userId, Long placeId, List waybleZoneIds) { - UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, userId) - .orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND)); + public int addZoneToPlaces(Long userId, List placeIds, Long waybleZoneId) { + User user = userRepository.findById(userId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.USER_NOT_FOUND)); + + WaybleZone zone = waybleZoneRepository.findById(waybleZoneId) + .orElseThrow(() -> new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND)); - Set uniqueIds = new LinkedHashSet<>(waybleZoneIds); + Set uniquePlaceIds = new LinkedHashSet<>(placeIds); int added = 0; - for (Long zoneId : uniqueIds) { - WaybleZone zone = waybleZoneRepository.findById(zoneId) - .orElseThrow(() -> new ApplicationException(UserErrorCase.WAYBLE_ZONE_NOT_FOUND)); + for (Long placeId : uniquePlaceIds) { + UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, user.getId()) + .orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND)); - boolean exists = mappingRepository.existsByUserPlace_IdAndWaybleZone_Id(placeId, zoneId); + boolean exists = mappingRepository.existsByUserPlace_IdAndWaybleZone_Id(placeId, waybleZoneId); if (exists) continue; mappingRepository.save(UserPlaceWaybleZoneMapping.builder() @@ -79,16 +81,18 @@ public void addZonesToPlace(Long userId, Long placeId, List waybleZoneIds) .build()); place.increaseCount(); - zone.addLikes(1); - waybleZoneRepository.save(zone); + userPlaceRepository.save(place); + zone.addLikes(1); // 리스트 하나에 추가될 때마다 +1 added++; } - if (added > 0) userPlaceRepository.save(place); + if (added > 0) { + waybleZoneRepository.save(zone); + } + return added; } - @Transactional(readOnly = true) public List getMyPlaceSummaries(Long userId, String sort) { userRepository.findById(userId) From 76198c7bd88b7a7947c7fbfd66a764b3431f9513 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 11:43:20 +0900 Subject: [PATCH 28/30] =?UTF-8?q?[refactor]=20=ED=95=B8=EB=93=A4=EB=9F=AC?= =?UTF-8?q?=20=EB=A9=94=EC=84=9C=EB=93=9C=EB=AA=85=20=EC=9D=98=EB=AF=B8?= =?UTF-8?q?=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wayble/server/user/controller/UserPlaceController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 a0a7151..7ca0bfc 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -94,7 +94,7 @@ public CommonResponse removeZoneFromPlace( @ApiResponse(responseCode = "200", description = "웨이블존에 리스트 추가 성공"), @ApiResponse(responseCode = "404", description = "유저/리스트/웨이블존을 찾을 수 없음") }) - public CommonResponse addZonesToPlace( + public CommonResponse addZoneToPlaces( @Parameter(hidden = true) @CurrentUser Long userId, @RequestBody @Valid UserPlaceAddZonesRequestDto request ) { From 89b59953993dcf1dbb556ff9a9e6ce11ccee6809 Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 11:58:17 +0900 Subject: [PATCH 29/30] =?UTF-8?q?[refactor]=20=EC=BD=94=EB=93=9C=EB=A6=AC?= =?UTF-8?q?=EB=B7=B0=20=EB=B0=98=EC=98=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../user/controller/UserPlaceController.java | 41 ++++++++++--------- .../server/user/service/UserPlaceService.java | 2 +- 2 files changed, 22 insertions(+), 21 deletions(-) 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 7ca0bfc..1f72bf2 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -69,26 +69,9 @@ public CommonResponse> getMyPlaceSummaries( return CommonResponse.success(summaries); } - @DeleteMapping("/zones") - @Operation( - summary = "내가 저장한 리스트에서 웨이블존 제거", - description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다." - ) - @ApiResponses({ - @ApiResponse(responseCode = "200", description = "제거 성공"), - @ApiResponse(responseCode = "404", description = "장소 또는 매핑 정보를 찾을 수 없음"), - @ApiResponse(responseCode = "403", description = "권한이 없습니다.") - }) - public CommonResponse removeZoneFromPlace( - @Parameter(hidden = true) @CurrentUser Long userId, - @RequestBody @Valid UserPlaceRemoveRequestDto request - ) { - userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId()); - return CommonResponse.success("성공적으로 제거되었습니다."); - } @PostMapping("/zones") - @Operation(summary = "웨이블존에 저장한 리스트 추가 (여러개 가능)", + @Operation(summary = "웨이블존에 저장한 리스트 추가 (여러 개 가능)", description = "웨이블존에 사용자가 요청한 리스트들을 추가합니다.") @ApiResponses({ @ApiResponse(responseCode = "200", description = "웨이블존에 리스트 추가 성공"), @@ -104,7 +87,7 @@ public CommonResponse addZoneToPlaces( @GetMapping("/zones") @Operation(summary = "저장한 리스트 내 웨이블존 목록 조회(페이징)", - description = "placeId로 해당 장소 내부의 웨이블존 목록을 반환합니다. (page는 0부터 시작.)") + description = "placeId로 해당 장소 내부의 웨이블존 목록을 반환합니다. (page는 1부터 시작.)") @ApiResponses({ @ApiResponse(responseCode = "200", description = "조회 성공"), @ApiResponse(responseCode = "404", description = "유저/장소를 찾을 수 없음"), @@ -113,10 +96,28 @@ public CommonResponse addZoneToPlaces( public CommonResponse> getZonesInPlace( @Parameter(hidden = true) @CurrentUser Long userId, @RequestParam Long placeId, - @RequestParam(defaultValue = "0") @Min(0) Integer page, + @RequestParam(defaultValue = "1") @Min(0) Integer page, @RequestParam(defaultValue = "20") @Min(1) @Max(100) Integer size ) { Page zones = userPlaceService.getZonesInPlace(userId, placeId, page, size); return CommonResponse.success(zones); } + + @DeleteMapping("/zones") + @Operation( + summary = "내가 저장한 리스트에서 웨이블존 제거", + description = "RequestBody로 placeId, waybleZoneId를 받아 지정한 장소에서 웨이블존을 제거합니다." + ) + @ApiResponses({ + @ApiResponse(responseCode = "200", description = "제거 성공"), + @ApiResponse(responseCode = "404", description = "장소 또는 매핑 정보를 찾을 수 없음"), + @ApiResponse(responseCode = "403", description = "권한이 없습니다.") + }) + public CommonResponse removeZoneFromPlace( + @Parameter(hidden = true) @CurrentUser Long userId, + @RequestBody @Valid UserPlaceRemoveRequestDto request + ) { + userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId()); + return CommonResponse.success("성공적으로 제거되었습니다."); + } } 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 af048e7..2be3deb 100644 --- a/src/main/java/com/wayble/server/user/service/UserPlaceService.java +++ b/src/main/java/com/wayble/server/user/service/UserPlaceService.java @@ -117,7 +117,7 @@ public Page getZonesInPlace(Long userId, Long placeId UserPlace place = userPlaceRepository.findByIdAndUser_Id(placeId, userId) .orElseThrow(() -> new ApplicationException(UserErrorCase.PLACE_NOT_FOUND)); - Pageable pageable = PageRequest.of(page, size, Sort.by(Sort.Direction.DESC, "id")); + Pageable pageable = PageRequest.of(page - 1, size, Sort.by(Sort.Direction.DESC, "id")); Page zones = mappingRepository.findZonesByPlaceId(place.getId(), pageable); return zones.map(z -> From fa9f0f8eaa939319d4c93419e94732d7b1e83e1c Mon Sep 17 00:00:00 2001 From: seungin Date: Wed, 20 Aug 2025 12:36:36 +0900 Subject: [PATCH 30/30] =?UTF-8?q?[fix]=20=ED=8E=98=EC=9D=B4=EC=A7=80=20?= =?UTF-8?q?=EA=B4=80=EB=A0=A8=200=20->=201=20=EB=A1=9C=20=EB=B3=80?= =?UTF-8?q?=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/wayble/server/user/controller/UserPlaceController.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 1f72bf2..ac89661 100644 --- a/src/main/java/com/wayble/server/user/controller/UserPlaceController.java +++ b/src/main/java/com/wayble/server/user/controller/UserPlaceController.java @@ -96,7 +96,7 @@ public CommonResponse addZoneToPlaces( public CommonResponse> getZonesInPlace( @Parameter(hidden = true) @CurrentUser Long userId, @RequestParam Long placeId, - @RequestParam(defaultValue = "1") @Min(0) Integer page, + @RequestParam(defaultValue = "1") @Min(1) Integer page, @RequestParam(defaultValue = "20") @Min(1) @Max(100) Integer size ) { Page zones = userPlaceService.getZonesInPlace(userId, placeId, page, size);