Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

verify 파라미터 타입 불일치 가능성 (1L vs 1)

서비스 구현은 zone.addReviewCount(1)을 호출합니다. 엔티티 메서드가 int 혹은 Integer를 받는 경우 현재 검증(1L)은 시그니처가 달라 실패할 수 있습니다. 테스트를 구현과 동일한 타입으로 맞춰주세요.

-        verify(zone, times(1)).addReviewCount(1L);
+        verify(zone, times(1)).addReviewCount(1);
🤖 Prompt for AI Agents
In src/test/java/com/wayble/server/review/service/ReviewServiceTest.java around
line 69, the verify call uses 1L which may not match the entity method signature
(service calls zone.addReviewCount(1)); change the verify to pass an int (use 1)
to match the implementation and avoid type mismatch in the Mockito verification.

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
Loading