Skip to content
Merged
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package com.wayble.server.auth.resolver;

import java.lang.annotation.*;

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CurrentUser {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package com.wayble.server.auth.resolver;

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.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
public class CurrentUserArgumentResolver implements HandlerMethodArgumentResolver {

@Override
public boolean supportsParameter(MethodParameter parameter) {
return parameter.hasParameterAnnotation(CurrentUser.class)
&& Long.class.equals(parameter.getParameterType());
}

@Override
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 {
return Long.parseLong(s);
} catch (NumberFormatException ignored) {}
}
try {
return Long.parseLong(auth.getName());
} catch (Exception e) {
throw new IllegalStateException("userId를 추출할 수 없습니다.", e);
}
}
}
21 changes: 21 additions & 0 deletions src/main/java/com/wayble/server/common/config/WebMvcConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.wayble.server.common.config;

import com.wayble.server.auth.resolver.CurrentUserArgumentResolver;
import lombok.RequiredArgsConstructor;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.method.support.HandlerMethodArgumentResolver;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

import java.util.List;

@Configuration
@RequiredArgsConstructor
public class WebMvcConfig implements WebMvcConfigurer {

private final CurrentUserArgumentResolver currentUserArgumentResolver;

@Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
resolvers.add(currentUserArgumentResolver);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.wayble.server.review.controller;

import com.wayble.server.auth.resolver.CurrentUser;
import com.wayble.server.common.response.CommonResponse;
import com.wayble.server.review.dto.ReviewRegisterDto;
import com.wayble.server.review.dto.ReviewResponseDto;
Expand All @@ -11,8 +12,6 @@
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;


Expand All @@ -38,9 +37,9 @@ public class ReviewController {
})
public CommonResponse<String> registerReview(
@PathVariable Long waybleZoneId,
@CurrentUser Long userId,
@RequestBody @Valid ReviewRegisterDto dto
) {
Long userId = extractUserId(); // 토큰에서 유저 ID 추출
reviewService.registerReview(waybleZoneId, userId, dto);
return CommonResponse.success("리뷰가 등록되었습니다.");
}
Expand All @@ -57,26 +56,4 @@ public CommonResponse<List<ReviewResponseDto>> getReviews(
) {
return CommonResponse.success(reviewService.getReviews(waybleZoneId, sort));
}

private Long extractUserId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) { throw new IllegalStateException("인증 정보가 없습니다."); }

Object p = auth.getPrincipal();
if (p instanceof Long l) { return l; }
if (p instanceof Integer i) { return i.longValue(); }
if (p instanceof String s) {
try {
return Long.parseLong(s);
} catch (NumberFormatException e) {
throw new IllegalStateException("principal에서 userId 파싱 실패");
}
}
try {
return Long.parseLong(auth.getName());
}
catch (Exception e) {
throw new IllegalStateException("인증 정보에서 userId를 추출할 수 없습니다.");
}
}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.wayble.server.user.controller;


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;
Expand All @@ -13,8 +14,6 @@
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.web.bind.annotation.*;

import java.util.List;
Expand All @@ -35,9 +34,9 @@ public class UserPlaceController {
@ApiResponse(responseCode = "403", description = "권한이 없습니다.")
})
public CommonResponse<String> saveUserPlace(
@CurrentUser Long userId,
@RequestBody @Valid UserPlaceRequestDto request
) {
Long userId = (Long) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
userPlaceService.saveUserPlace(userId, request); // userId 파라미터로 넘김
return CommonResponse.success("장소가 저장되었습니다.");
}
Expand All @@ -50,9 +49,9 @@ public CommonResponse<String> saveUserPlace(
@ApiResponse(responseCode = "403", description = "권한이 없습니다.")
})
public CommonResponse<List<UserPlaceSummaryDto>> getMyPlaceSummaries(
@CurrentUser Long userId,
@RequestParam(name = "sort", defaultValue = "latest") String sort
) {
Long userId = extractUserId();
List<UserPlaceSummaryDto> summaries = userPlaceService.getMyPlaceSummaries(userId, sort);
return CommonResponse.success(summaries);
}
Expand All @@ -67,11 +66,11 @@ public CommonResponse<List<UserPlaceSummaryDto>> getMyPlaceSummaries(
@ApiResponse(responseCode = "403", description = "권한이 없습니다.")
})
public CommonResponse<Page<WaybleZoneListResponseDto>> getZonesInPlace(
@CurrentUser Long userId,
@RequestParam Long placeId,
@RequestParam(defaultValue = "1") Integer page,
@RequestParam(defaultValue = "20") Integer size
) {
Long userId = extractUserId();
Page<WaybleZoneListResponseDto> zones = userPlaceService.getZonesInPlace(userId, placeId, page, size);
return CommonResponse.success(zones);
}
Expand All @@ -86,37 +85,11 @@ public CommonResponse<Page<WaybleZoneListResponseDto>> getZonesInPlace(
@ApiResponse(responseCode = "404", description = "장소 또는 매핑 정보를 찾을 수 없음"),
@ApiResponse(responseCode = "403", description = "권한이 없습니다.")
})
public CommonResponse<String> removeZoneFromPlace(@RequestBody @Valid UserPlaceRemoveRequestDto request) {
Long userId = extractUserId();
public CommonResponse<String> removeZoneFromPlace(
@CurrentUser Long userId,
@RequestBody @Valid UserPlaceRemoveRequestDto request
) {
userPlaceService.removeZoneFromPlace(userId, request.placeId(), request.waybleZoneId());
return CommonResponse.success("제거되었습니다.");
}


// SecurityContext에서 userId 추출하는 로직
private Long extractUserId() {
Authentication auth = SecurityContextHolder.getContext().getAuthentication();
if (auth == null) {
throw new IllegalStateException("인증 정보가 없습니다.");
}

Object p = auth.getPrincipal();

if (p instanceof Long l) { return l; }
if (p instanceof Integer i) { return i.longValue(); }
if (p instanceof String s) {
try {
return Long.parseLong(s);
} catch (NumberFormatException e) {
// 숫자 변환 실패 시 출력
System.err.println("Principal 문자열을 Long으로 변환할 수 없습니다: " + s);
}
}

try {
return Long.parseLong(auth.getName());
} catch (Exception e) {
throw new IllegalStateException("인증 정보에서 userId를 추출할 수 없습니다. Principal=" + p, e);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ public record WaybleZoneDetailResponseDto(
String imageUrl,
FacilityDto facilities,
Map<String, BusinessHourDto> businessHours,
List<String> photos
List<String> photos,
Double latitude,
Double longitude
) {
@Builder
public record BusinessHourDto(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ public WaybleZoneDetailResponseDto getWaybleZoneDetail(Long waybleZoneId) {
.build());
}

Double lat = zone.getAddress() != null ? zone.getAddress().getLatitude() : null;
Double lon = zone.getAddress() != null ? zone.getAddress().getLongitude() : null;

return WaybleZoneDetailResponseDto.builder()
.waybleZoneId(zone.getId())
.name(zone.getZoneName())
Expand All @@ -102,6 +105,8 @@ public WaybleZoneDetailResponseDto getWaybleZoneDetail(Long waybleZoneId) {
.floorInfo(f.getFloorInfo())
.build())
.businessHours(businessHours)
.latitude(lat)
.longitude(lon)
.build();
}

Expand Down
106 changes: 106 additions & 0 deletions src/test/java/com/wayble/server/review/service/ReviewServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package com.wayble.server.review.service;

import com.wayble.server.common.exception.ApplicationException;
import com.wayble.server.review.dto.ReviewRegisterDto;
import com.wayble.server.review.entity.Review;
import com.wayble.server.review.entity.ReviewImage;
import com.wayble.server.review.repository.ReviewImageRepository;
import com.wayble.server.review.repository.ReviewRepository;
import com.wayble.server.user.entity.User;
import com.wayble.server.user.repository.UserRepository;
import com.wayble.server.wayblezone.entity.WaybleZone;
import com.wayble.server.wayblezone.repository.WaybleZoneRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;

import java.time.LocalDate;
import java.util.List;
import java.util.Optional;

import static org.junit.jupiter.api.Assertions.*;
import static org.mockito.Mockito.*;

class ReviewServiceTest {

private final ReviewRepository reviewRepository = mock(ReviewRepository.class);
private final ReviewImageRepository reviewImageRepository = mock(ReviewImageRepository.class);
private final WaybleZoneRepository waybleZoneRepository = mock(WaybleZoneRepository.class);
private final UserRepository userRepository = mock(UserRepository.class);

private final ReviewService sut =
new ReviewService(reviewRepository, reviewImageRepository, waybleZoneRepository, userRepository);

@Test
@DisplayName("리뷰 등록 성공 - 평점 갱신, 카운트 증가, 이미지 저장")
void t1() {
Long zoneId = 10L;
Long userId = 5L;

WaybleZone zone = mock(WaybleZone.class);
when(waybleZoneRepository.findById(zoneId)).thenReturn(Optional.of(zone));
when(zone.getRating()).thenReturn(4.0);
when(zone.getReviewCount()).thenReturn(1L);

User user = mock(User.class);
when(userRepository.findById(userId)).thenReturn(Optional.of(user));

ReviewRegisterDto dto = new ReviewRegisterDto(
"뷰가 좋고 접근성이 좋아요",
5.0,
LocalDate.of(2025, 6, 30),
List.of("주차장 있음", "장애인 화장실 있음"),
List.of("https://image.url/review1.jpg")
);

doAnswer(invocation -> invocation.getArgument(0))
.when(reviewRepository).save(any(Review.class));

sut.registerReview(zoneId, userId, dto);


verify(reviewRepository, times(1)).save(any(Review.class));

ArgumentCaptor<Double> ratingCaptor = ArgumentCaptor.forClass(Double.class);
verify(zone, times(1)).updateRating(ratingCaptor.capture());

assertEquals(4.5, ratingCaptor.getValue(), 1e-6);

verify(zone, times(1)).addReviewCount(1L);
verify(reviewImageRepository, times(1)).save(any(ReviewImage.class));
verify(waybleZoneRepository, times(1)).save(zone);
}

@Test
@DisplayName("리뷰 등록 실패 - 웨이블존 없음")
void t2() {
Long zoneId = 99L;
Long userId = 1L;
when(waybleZoneRepository.findById(zoneId)).thenReturn(Optional.empty());

ReviewRegisterDto dto = new ReviewRegisterDto(
"좋아요", 4.0, LocalDate.now(), List.of("주차장"), List.of()
);

assertThrows(ApplicationException.class,
() -> sut.registerReview(zoneId, userId, dto));
}

@Test
@DisplayName("리뷰 등록 실패 - 유저 없음")
void t3() {
Long zoneId = 10L;
Long userId = 999L;

WaybleZone zone = mock(WaybleZone.class);
when(waybleZoneRepository.findById(zoneId)).thenReturn(Optional.of(zone));
when(userRepository.findById(userId)).thenReturn(Optional.empty());

ReviewRegisterDto dto = new ReviewRegisterDto(
"좋아요", 4.0, LocalDate.now(), List.of("주차장"), List.of()
);

assertThrows(ApplicationException.class,
() -> sut.registerReview(zoneId, userId, dto));
}
}
Loading