Skip to content

Commit

Permalink
Merge pull request #50 from 2E1I/dev
Browse files Browse the repository at this point in the history
최신 검색어 개발 완료, validation 오류 해결
  • Loading branch information
CEO-Nick authored May 27, 2024
2 parents c06d2d1 + 36ce769 commit cbe926a
Show file tree
Hide file tree
Showing 11 changed files with 182 additions and 35 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,12 @@ public enum ErrorCode{
LIKE_NOT_FOUND(94040,HttpStatus.NOT_FOUND,"사용자가 모음집을 좋아요한 기록이 없습니다."),

//친구 관련 에러 코드
FRINEDS_NOT_FOUND(104040, HttpStatus.NOT_FOUND, "해당 사용자와 친구가 아닙니다.");
FRIENDS_NOT_FOUND(104040, HttpStatus.NOT_FOUND, "해당 사용자와 친구가 아닙니다."),

// 최근 검색 목록 관련 에러 코드
INDEX_OUT_OF_RANGE(114000, HttpStatus.BAD_REQUEST, "인덱스 범위를 벗어난 요청입니다."),

;
private final int errorCode;
private final HttpStatus httpStatusCode;
private final String description;
Expand Down
36 changes: 36 additions & 0 deletions src/main/java/com/e2i1/linkeepserver/config/RedisConfig.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package com.e2i1.linkeepserver.config;

import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

@Value("${spring.data.redis.host}")
private String host;

@Value("${spring.data.redis.port}")
private int port;

@Bean
public RedisConnectionFactory redisConnectionFactory(){
return new LettuceConnectionFactory(host, port);
}

@Bean
public RedisTemplate<String, String> redisTemplate() {
RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(redisConnectionFactory());
redisTemplate.setKeySerializer(new StringRedisSerializer());
redisTemplate.setValueSerializer(new StringRedisSerializer());
redisTemplate.setHashKeySerializer(new StringRedisSerializer());
redisTemplate.setHashValueSerializer(new StringRedisSerializer());
return redisTemplate;
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ public FriendsEntity insertFriend(FriendsEntity friend) {
}

public FriendsEntity findByFollowedUserAndFollowingUser(UsersEntity followee, UsersEntity follower) {
return friendsRepository.findByFollowedUserAndFollowingUser(followee,follower).orElseThrow(() -> new ApiException(ErrorCode.FRINEDS_NOT_FOUND));
return friendsRepository.findByFollowedUserAndFollowingUser(followee,follower).orElseThrow(() -> new ApiException(ErrorCode.FRIENDS_NOT_FOUND));
}


Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,8 @@
import java.util.Optional;
import java.util.regex.Pattern;
import java.util.stream.Collectors;

import com.e2i1.linkeepserver.domain.users.service.RecentSearchService;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
Expand All @@ -37,6 +39,8 @@ public class LinksBusiness {

private final CollaboratorsService collaboratorsService;

private final RecentSearchService recentSearchService;

// 사전에 정규 표현식 컴파일 해놓고 재사용하기
final Pattern BLANK_PATTERN = Pattern.compile("\\s+");

Expand Down Expand Up @@ -82,7 +86,10 @@ public LinkResDTO findOneById(Long linkId, Long userId) {
/**
* 링크 title, description을 조회해 해당 검색어 들어있는 링크 목록 가져오기
*/
public SearchLinkResDTO searchLinks(String keyword, Long view, Long lastId, Integer size) {
public SearchLinkResDTO searchLinks(Long userId, String keyword, Long view, Long lastId, Integer size) {
// redis에 유저의 최근 검색어 목록에 검색어 저장하기
recentSearchService.addSearchTerm(userId, keyword);

// 검색어를 공백 제외하고 하나의 문자열로 변환
keyword = BLANK_PATTERN.matcher(keyword).replaceAll("").toLowerCase();

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -33,10 +33,11 @@ public ResponseEntity<SearchLinkResDTO> searchLink(
@RequestParam(value = "search") String keyword,
@RequestParam(value = "view", required = false) Long view,
@RequestParam(value = "lastId", required = false) Long lastId,
@RequestParam(value = "size", defaultValue = DEFAULT_PAGE_SIZE) Integer size
@RequestParam(value = "size", defaultValue = DEFAULT_PAGE_SIZE) Integer size,
@UserSession UsersEntity user
) {
log.info("search keyword = {}", keyword);
SearchLinkResDTO searchLinks = linksBusiness.searchLinks(keyword, view, lastId, size);
SearchLinkResDTO searchLinks = linksBusiness.searchLinks(user.getId(), keyword, view, lastId, size);
return ResponseEntity.ok(searchLinks);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.e2i1.linkeepserver.domain.links.dto;

import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
Expand All @@ -19,6 +20,6 @@ public class LinkReqDTO {

private String description;

@NotBlank(message = "저장할 모음집을 선택하세요.")
@NotNull(message = "저장할 모음집을 선택하세요.")
private Long collectionId;
}
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,9 @@
import com.e2i1.linkeepserver.domain.token.entity.BlackList;
import com.e2i1.linkeepserver.domain.token.service.TokenService;
import com.e2i1.linkeepserver.domain.users.converter.UsersConverter;
import com.e2i1.linkeepserver.domain.users.dto.EditProfileReqDTO;
import com.e2i1.linkeepserver.domain.users.dto.LinkHomeResDTO;
import com.e2i1.linkeepserver.domain.users.dto.LoginReqDTO;
import com.e2i1.linkeepserver.domain.users.dto.LoginResDTO;
import com.e2i1.linkeepserver.domain.users.dto.NicknameResDTO;
import com.e2i1.linkeepserver.domain.users.dto.ProfileResDTO;
import com.e2i1.linkeepserver.domain.users.dto.SignupReqDTO;
import com.e2i1.linkeepserver.domain.users.dto.UserHomeResDTO;
import com.e2i1.linkeepserver.domain.users.dto.*;
import com.e2i1.linkeepserver.domain.users.entity.UsersEntity;
import com.e2i1.linkeepserver.domain.users.service.RecentSearchService;
import com.e2i1.linkeepserver.domain.users.service.UsersService;
import java.util.List;
import java.util.Random;
Expand All @@ -45,6 +39,8 @@ public class UsersBusiness {
private final S3ImageService s3ImageService;
private final TokenService tokenService;

private final RecentSearchService recentSearchService;

@Transactional
public LoginResDTO login(LoginReqDTO loginReqDTO) {
UsersEntity user = usersService.getUser(loginReqDTO.getEmail());
Expand Down Expand Up @@ -207,5 +203,15 @@ private String createRandomNickname() {
return nickname;
}

public RecentSearchResDTO getRecentSearch(Long userID) {
return RecentSearchResDTO.builder()
.recentSearchList(recentSearchService.getRecentSearch(userID))
.build();
}

public void deleteRecentKeyword(Long userId, int index) {
recentSearchService.deleteRecentSearch(userId, index);
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -5,28 +5,14 @@
import com.e2i1.linkeepserver.common.annotation.UserSession;
import com.e2i1.linkeepserver.domain.token.dto.TokenResDTO;
import com.e2i1.linkeepserver.domain.users.business.UsersBusiness;
import com.e2i1.linkeepserver.domain.users.dto.EditProfileReqDTO;
import com.e2i1.linkeepserver.domain.users.dto.LoginReqDTO;
import com.e2i1.linkeepserver.domain.users.dto.LoginResDTO;
import com.e2i1.linkeepserver.domain.users.dto.NicknameResDTO;
import com.e2i1.linkeepserver.domain.users.dto.ProfileResDTO;
import com.e2i1.linkeepserver.domain.users.dto.SignupReqDTO;
import com.e2i1.linkeepserver.domain.users.dto.UserHomeResDTO;
import com.e2i1.linkeepserver.domain.users.dto.*;
import com.e2i1.linkeepserver.domain.users.entity.UsersEntity;
import jakarta.validation.Valid;
import java.util.List;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PatchMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RequestPart;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;

@Slf4j
Expand Down Expand Up @@ -101,5 +87,22 @@ public ResponseEntity<String> logout(@RequestHeader("authorization-token") Strin
return ResponseEntity.ok("로그아웃 되었습니다.");
}

@GetMapping("/recent-search")
public ResponseEntity<RecentSearchResDTO> recentSearch(@UserSession UsersEntity user) {
RecentSearchResDTO response = usersBusiness.getRecentSearch(user.getId());

return ResponseEntity.ok(response);
}

@DeleteMapping("/recent-search")
public ResponseEntity<String> deleteRecentSearch(
@RequestParam int index,
@UserSession UsersEntity user) {

usersBusiness.deleteRecentKeyword(user.getId(), index);

return ResponseEntity.ok("success");
}


}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
package com.e2i1.linkeepserver.domain.users.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class RecentSearchResDTO {
private List<String> recentSearchList;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
package com.e2i1.linkeepserver.domain.users.service;

import com.e2i1.linkeepserver.common.exception.ApiException;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.ListOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.List;

import static com.e2i1.linkeepserver.common.error.ErrorCode.INDEX_OUT_OF_RANGE;
import static com.e2i1.linkeepserver.common.error.ErrorCode.NULL_POINT;

@Service
@RequiredArgsConstructor
public class RecentSearchService {

private final RedisTemplate<String, String> redisTemplate;
private static final String RECENT_SEARCHES_KEY_PREFIX = "recent_searches:";
private static final int MAX_RECENT_SEARCHES = 10;
private static final String DELETE_MARKER = "__DELETE__";


public void addSearchTerm(Long userId, String keyword) {
String key = RECENT_SEARCHES_KEY_PREFIX + userId;

List<String> searchList = redisTemplate.opsForList().range(key, 0, -1);

if (searchList == null) {
throw new ApiException(NULL_POINT, "해당 유저는 최근 검색 기록이 없습니다.");
}

// keyword가 이미 최근 검색어 목록에 있다면 중복 저장하지 않고 가장 앞으로 가져오기 위해 삭제
if (searchList.contains(keyword)) {
redisTemplate.opsForList().remove(key, 0, keyword);
}

redisTemplate.opsForList().leftPush(key, keyword);

redisTemplate.opsForList().trim(key, 0, MAX_RECENT_SEARCHES - 1);
}

public List<String> getRecentSearch(Long userId) {
String key = RECENT_SEARCHES_KEY_PREFIX + userId;
return redisTemplate.opsForList().range(key, 0, -1);

}

public void deleteRecentSearch(Long userId, int index) {
String key = RECENT_SEARCHES_KEY_PREFIX + userId;
ListOperations<String, String> listOps = redisTemplate.opsForList();
Long listSize = listOps.size(key);

if (listSize == null) {
throw new ApiException(NULL_POINT, "해당 유저는 최근 검색 기록이 없습니다.");
}
if (index >= listSize || index < 0) {
throw new ApiException(INDEX_OUT_OF_RANGE);
}

listOps.set(key, index, DELETE_MARKER);
listOps.remove(key, 0, DELETE_MARKER); //list 개수 10개 고정이기에 전체 탐색 오버헤드 작음
}


}
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,11 @@
import com.e2i1.linkeepserver.common.error.ErrorResponse;
import com.e2i1.linkeepserver.common.exception.ApiException;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.support.BeanDefinitionValidationException;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

Expand Down Expand Up @@ -36,20 +37,26 @@ public ResponseEntity<ErrorResponse> handleApiException(ApiException ex) {
* DTO 등에서 validation 실패 시, 해당 예외 처리하는 핸들러
* NotNull, NotBlank 등의 애노테이션 검증 실패 시 해당 예외 처리해줌
*/
@ExceptionHandler(value = BeanDefinitionValidationException.class)
public ResponseEntity<ErrorResponse> handlerBeanValidationException(BeanDefinitionValidationException ex) {
log.error("", ex);
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleMethodArgumentNotValidException(
MethodArgumentNotValidException ex) {
log.error("Method argument not valid exception", ex);

FieldError fieldError = ex.getBindingResult().getFieldError();
String errorMessage = (fieldError != null) ? fieldError.getDefaultMessage() : "Validation error";

return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(
ErrorResponse.builder()
.errorCode(40000)
.errorMessage(ex.getMessage())
.errorMessage(errorMessage)
.build()
);
}

// 예상치 못한 예외에 대응하기 위한 Exception handler
// TODO : 최종 어플리케이션 배포 시, ex.getMessage() 대신 "SERVER ERROR"로 수정
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleException(Exception ex) {
return ResponseEntity
Expand Down

0 comments on commit cbe926a

Please sign in to comment.