Skip to content
Open
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
4 changes: 4 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,10 @@ dependencies {
// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1'

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.Spring_Boot.domain.review.controller;

import com.example.Spring_Boot.domain.mission.dto.MissionResDTO;
import com.example.Spring_Boot.domain.review.dto.ReviewCursor;
import com.example.Spring_Boot.domain.review.dto.ReviewReqDTO;
import com.example.Spring_Boot.domain.review.dto.ReviewResDTO;
import com.example.Spring_Boot.domain.review.service.ReviewService;
Expand All @@ -17,7 +18,6 @@ public class ReviewController {

private final ReviewService reviewService;

// 기존: 리뷰 작성 + 미션 3: @Valid 추가
@PostMapping("/stores/{storeId}/reviews")
public ApiResponse<ReviewResDTO.CreateReviewResultDTO> createReview(
@PathVariable Long storeId,
Expand All @@ -28,17 +28,13 @@ public ApiResponse<ReviewResDTO.CreateReviewResultDTO> createReview(
reviewService.createReview(userId, storeId, request));
}

// 미션 2: 내가 생성한 리뷰 조회 (커서 기반)
// GET /api/reviews/my?userId=1&pageSize=10&cursor=-1&query=id
// query: id(ID순) 또는 rating(별점순)
@GetMapping("/reviews/my")
public ApiResponse<MissionResDTO.Pagination<ReviewResDTO.GetReview>> getMyReviews(
@RequestParam Long userId,
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

아마 다음 주차에 나올 것 같긴한데, JWT를 도입하게 되면 @AuthenticationPrincipal를 사용해서 시큐리티컨텍스트에 저장된 객체를 빼서 주입해줄 수 있습니다. 이렇게 사용하면 userId 파라미터에 의존하고 있는 현재 구조를 더 안정적으로 바꿀 수 있다는 장점이 있는데, 다음주에 코드 수정하실때 한번 확인하고 해주시면 좋을 것 같아서 리뷰 남겨봅니당

@RequestParam(defaultValue = "10") Integer pageSize,
@RequestParam(defaultValue = "-1") String cursor,
@RequestParam(defaultValue = "id") String query
ReviewCursor reviewCursor
) {
return ApiResponse.onSuccess(GeneralSuccessCode.OK,
reviewService.getMyReviews(userId, pageSize, cursor, query));
reviewService.getMyReviews(userId, pageSize, reviewCursor));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
package com.example.Spring_Boot.domain.review.dto;

import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
public class ReviewCursor {
private final String query;
private final String cursor;
private final Long idCursor;
private final Integer ratingCursor;
private final boolean hasCursor;
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@
import com.example.Spring_Boot.domain.mission.converter.MissionConverter;
import com.example.Spring_Boot.domain.mission.dto.MissionResDTO;
import com.example.Spring_Boot.domain.review.converter.ReviewConverter;
import com.example.Spring_Boot.domain.review.dto.ReviewCursor;
import com.example.Spring_Boot.domain.review.dto.ReviewReqDTO;
import com.example.Spring_Boot.domain.review.dto.ReviewResDTO;
import com.example.Spring_Boot.domain.review.entity.Review;
Expand All @@ -28,7 +29,6 @@ public class ReviewService {
private final UserRepository userRepository;
private final StoreRepository storeRepository;

// 기존: 리뷰 작성
public ReviewResDTO.CreateReviewResultDTO createReview(Long userId, Long storeId,
ReviewReqDTO.CreateReviewDTO request) {
User user = userRepository.findById(userId)
Expand All @@ -39,45 +39,27 @@ public ReviewResDTO.CreateReviewResultDTO createReview(Long userId, Long storeId
return ReviewConverter.toCreateReviewResultDTO(reviewRepository.save(review));
}

// 미션 2: 내가 생성한 리뷰 조회 (커서 기반)
@Transactional(readOnly = true)
public MissionResDTO.Pagination<ReviewResDTO.GetReview> getMyReviews(
Long userId, Integer pageSize, String cursor, String query) {
Long userId, Integer pageSize, ReviewCursor reviewCursor) {

PageRequest pageRequest = PageRequest.of(0, pageSize);

long idCursor;
Slice<Review> reviewList;
String nextCursor;

// 커서가 있는 경우
if (!cursor.equals("-1")) {

// 커서 분리: "id:10" or "rating:4:10"
String[] cursorSplit = cursor.split(":");

switch (query.toLowerCase()) {
case "id" -> {
// 커서 타입 변환
idCursor = Long.parseLong(cursorSplit[1]);
// ID 순 조회
reviewList = reviewRepository.findReviewsByUserIdAndIdLessThanOrderByIdDesc(
userId, idCursor, pageRequest);
}
case "rating" -> {
// 커서 타입 변환
Integer ratingCursor = Integer.parseInt(cursorSplit[1]);
idCursor = Long.parseLong(cursorSplit[2]);
// 별점 순 조회 (복합 커서)
reviewList = reviewRepository
.findReviewsByUserIdAndRatingLessThanOrUserIdAndRatingEqualsAndIdLessThanOrderByRatingDescIdDesc(
userId, ratingCursor, userId, ratingCursor, idCursor, pageRequest);
}
if (reviewCursor.isHasCursor()) {
switch (reviewCursor.getQuery().toLowerCase()) {
case "id" -> reviewList = reviewRepository
.findReviewsByUserIdAndIdLessThanOrderByIdDesc(
userId, reviewCursor.getIdCursor(), pageRequest);
case "rating" -> reviewList = reviewRepository
.findReviewsByUserIdAndRatingLessThanOrUserIdAndRatingEqualsAndIdLessThanOrderByRatingDescIdDesc(
userId, reviewCursor.getRatingCursor(),
userId, reviewCursor.getRatingCursor(),
reviewCursor.getIdCursor(), pageRequest);
default -> throw new ProjectException(GeneralErrorCode.BAD_REQUEST);
}
} else {
// 커서 없이 조회
switch (query.toLowerCase()) {
switch (reviewCursor.getQuery().toLowerCase()) {
case "id" -> reviewList = reviewRepository
.findReviewsByUserIdOrderByIdDesc(userId, pageRequest);
case "rating" -> reviewList = reviewRepository
Expand All @@ -86,18 +68,16 @@ public MissionResDTO.Pagination<ReviewResDTO.GetReview> getMyReviews(
}
}

// 다음 커서 계산: ID:ID 형태 or rating:rating:ID 형태
Review lastReview = reviewList.getContent().isEmpty() ? null
: reviewList.getContent().get(reviewList.getContent().size() - 1);

nextCursor = lastReview == null ? null : switch (query.toLowerCase()) {
String nextCursor = lastReview == null ? null : switch (reviewCursor.getQuery().toLowerCase()) {
case "id" -> lastReview.getId() + ":" + lastReview.getId();
case "rating" -> lastReview.getRating() + ":" + lastReview.getRating() + ":" + lastReview.getId();
default -> null;
};

List<ReviewResDTO.GetReview> data = reviewList.map(ReviewConverter::toGetReview).toList();

return MissionConverter.toPagination(data, reviewList.hasNext(), nextCursor, reviewList.getSize());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
import com.example.Spring_Boot.domain.user.service.UserService;
import com.example.Spring_Boot.global.apiPayload.ApiResponse;
import com.example.Spring_Boot.global.apiPayload.code.GeneralSuccessCode;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

Expand All @@ -15,14 +16,15 @@ public class UserController {

private final UserService userService;

// Public API - 회원가입
@PostMapping("/users")
public ApiResponse<UserResDTO.JoinResultDTO> join(
@RequestBody UserReqDTO.JoinDTO request
@Valid @RequestBody UserReqDTO.JoinDTO request
) {
return ApiResponse.onSuccess(GeneralSuccessCode.CREATED, null);
return ApiResponse.onSuccess(GeneralSuccessCode.CREATED, userService.join(request));
}

// 화면 3: 마이페이지
// Private API - 마이페이지
@GetMapping("/users/{userId}")
public ApiResponse<UserResDTO.MyPageDTO> getMyPage(
@PathVariable Long userId
Expand Down
Original file line number Diff line number Diff line change
@@ -1,10 +1,37 @@
package com.example.Spring_Boot.domain.user.converter;

import com.example.Spring_Boot.domain.mission.enums.Address;
import com.example.Spring_Boot.domain.user.dto.UserReqDTO;
import com.example.Spring_Boot.domain.user.dto.UserResDTO;
import com.example.Spring_Boot.domain.user.entity.User;
import com.example.Spring_Boot.domain.user.enums.Gender;
import com.example.Spring_Boot.domain.user.enums.SocialType;
import java.time.LocalDate;

public class UserConverter {

public static User toUser(UserReqDTO.JoinDTO request, String encodedPassword) {
return User.builder()
.name(request.getName())
.email(request.getEmail())
.password(encodedPassword)
.gender(request.getGender() != null ? Gender.valueOf(request.getGender()) : Gender.NONE)
.birth(LocalDate.now())
.address(Address.SEOUL)
.detailAddress("")
.socialUid("")
.socialType(SocialType.NONE)
.build();
}

public static UserResDTO.JoinResultDTO toJoinResultDTO(User user) {
return UserResDTO.JoinResultDTO.builder()
.userId(user.getId())
.name(user.getName())
.email(user.getEmail())
.build();
}

public static UserResDTO.MyPageDTO toMyPageDTO(User user) {
return UserResDTO.MyPageDTO.builder()
.userId(user.getId())
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package com.example.Spring_Boot.domain.user.dto;

import jakarta.validation.constraints.Email;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
import java.util.List;
Expand All @@ -9,7 +11,17 @@ public class UserReqDTO {
@Getter
@NoArgsConstructor
public static class JoinDTO {

@NotBlank(message = "이름은 필수입니다.")
private String name;

@NotBlank(message = "이메일은 필수입니다.")
@Email(message = "이메일 형식이 올바르지 않습니다.")
private String email;

@NotBlank(message = "비밀번호는 필수입니다.")
private String password;

private String gender;
private String birthDate;
private String address;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ public class UserResDTO {
public static class JoinResultDTO {
private Long userId;
private String name;
private String email;
}

@Getter
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ public class User extends BaseEntity {
@Column(name = "name", nullable = false)
private String name;

@Column(name = "email", nullable = false, unique = true)
private String email;

@Column(name = "password", nullable = false)
private String password;

@Column(name = "gender", nullable = false)
@Enumerated(EnumType.STRING)
private Gender gender;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
package com.example.Spring_Boot.domain.user.enums;

public enum SocialType {
KAKAO, NAVER, GOOGLE
KAKAO, NAVER, GOOGLE, NONE
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,8 @@

import com.example.Spring_Boot.domain.user.entity.User;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;

public interface UserRepository extends JpaRepository<User, Long> {
Optional<User> findByEmail(String email);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
package com.example.Spring_Boot.domain.user.security;

import com.example.Spring_Boot.domain.user.entity.User;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import java.util.Collection;
import java.util.List;

public class AuthMember implements UserDetails {

private final User user;

public AuthMember(User user) {
this.user = user;
}

public User getUser() {
return user;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.of();
}

@Override
public String getPassword() {
return user.getPassword();
}

@Override
public String getUsername() {
return user.getEmail();
}

@Override
public boolean isAccountNonExpired() { return true; }

@Override
public boolean isAccountNonLocked() { return true; }

@Override
public boolean isCredentialsNonExpired() { return true; }

@Override
public boolean isEnabled() { return true; }
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.example.Spring_Boot.domain.user.security;

import com.example.Spring_Boot.domain.user.entity.User;
import com.example.Spring_Boot.domain.user.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
User user = userRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("사용자를 찾을 수 없습니다."));
return new AuthMember(user);
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,14 @@
package com.example.Spring_Boot.domain.user.service;

import com.example.Spring_Boot.domain.user.converter.UserConverter;
import com.example.Spring_Boot.domain.user.dto.UserReqDTO;
import com.example.Spring_Boot.domain.user.dto.UserResDTO;
import com.example.Spring_Boot.domain.user.entity.User;
import com.example.Spring_Boot.domain.user.repository.UserRepository;
import com.example.Spring_Boot.global.apiPayload.code.GeneralErrorCode;
import com.example.Spring_Boot.global.exception.ProjectException;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

Expand All @@ -16,6 +18,14 @@
public class UserService {

private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;

@Transactional
public UserResDTO.JoinResultDTO join(UserReqDTO.JoinDTO request) {
String encodedPassword = passwordEncoder.encode(request.getPassword());
User user = UserConverter.toUser(request, encodedPassword);
return UserConverter.toJoinResultDTO(userRepository.save(user));
}

public UserResDTO.MyPageDTO getMyPage(Long userId) {
User user = userRepository.findById(userId)
Expand Down
Loading