Skip to content

Commit

Permalink
Merge pull request #71 from Solucitation/refactor/search-history
Browse files Browse the repository at this point in the history
Refactor/search history
  • Loading branch information
yeonjae02 authored Jul 30, 2024
2 parents f2fcdc0 + d49bb29 commit b79157e
Show file tree
Hide file tree
Showing 8 changed files with 355 additions and 0 deletions.
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
package com.solucitation.midpoint_backend.domain.history2.api;

import com.fasterxml.jackson.core.JsonParser;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.solucitation.midpoint_backend.domain.history2.dto.PlaceDtoV2;
import com.solucitation.midpoint_backend.domain.history2.dto.SearchHistoryRequestDtoV2;
import com.solucitation.midpoint_backend.domain.history2.dto.SearchHistoryResponseDtoV2;
import com.solucitation.midpoint_backend.domain.history2.service.SearchHistoryServiceV2;
import com.solucitation.midpoint_backend.domain.member.dto.ValidationErrorResponse;
import com.solucitation.midpoint_backend.domain.member.entity.Member;
import com.solucitation.midpoint_backend.domain.member.service.MemberService;
import jakarta.validation.ConstraintViolation;
import jakarta.validation.Validator;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.Authentication;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;

@Slf4j
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/search-history-v2")
public class SearchHistoryControllerV2 {
private final MemberService memberService;
private final Validator validator;
private final SearchHistoryServiceV2 searchHistoryService;

/**
*
* @param authentication 인증 정보
* @param request 검색 기록 저장에 필요한 정보 (동 정보, 장소 리스트)
* @return 장소를 성공적으로 저장하면 201 CREATED를 반환합니다.
* 로그인을 하지 않고 시도 시 401 UNAUTHORIZED를 반환합니다.
* 저장하려는 장소 정보에 문제가 있을 때 (필드가 공백이거나 null일 경우) 400 BAD_REQUEST를 반환합니다.
* 기타 사유로 저장 실패 시 500 INTERNAL_SERVER_ERROR를 반환합니다.
*/
@PostMapping("")
public ResponseEntity<Object> saveHistory(Authentication authentication,
@RequestBody SearchHistoryRequestDtoV2 request) {
try {
String neighborhood = request.getNeighborhood();
List<PlaceDtoV2> searchHistoryRequestDtos = request.getHistoryDto();

System.out.println("ssearchHistoryRequestDtos.size()= " +searchHistoryRequestDtos.size());
for (PlaceDtoV2 searchHistoryRequestDto : searchHistoryRequestDtos) {
System.out.println("searchHistoryRequestDto = " + searchHistoryRequestDto.getPlaceId());
}

// neighborhood 검증
if (neighborhood == null || neighborhood.trim().isEmpty()) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(Map.of("error", "EMPTY_FIELD", "message", "동 정보가 누락되었습니다."));
}

// 인증 검증
if (authentication == null || !authentication.isAuthenticated()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "UNAUTHORIZED", "message", "해당 서비스를 이용하기 위해서는 로그인이 필요합니다."));
}

String memberEmail = authentication.getName();
Member member = memberService.getMemberByEmail(memberEmail);

// 리스트 내의 모든 요소에 대한 검증 수행
for (PlaceDtoV2 place : searchHistoryRequestDtos) {
Set<ConstraintViolation<PlaceDtoV2>> violations = validator.validate(place);
if (!violations.isEmpty()) {
List<ValidationErrorResponse.FieldError> fieldErrors = violations.stream()
.map(violation -> new ValidationErrorResponse.FieldError(violation.getPropertyPath().toString(), violation.getMessage()))
.collect(Collectors.toList());
ValidationErrorResponse errorResponse = new ValidationErrorResponse(fieldErrors);
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(errorResponse);
}
}

// 데이터 저장
searchHistoryService.save(neighborhood, memberEmail, searchHistoryRequestDtos);
return ResponseEntity.status(HttpStatus.CREATED).body(Map.of("message", "장소를 저장하였습니다."));

} catch (IllegalArgumentException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(e.getMessage());
} catch (RuntimeException e) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", "USER_NOT_FOUND", "message", "사용자를 찾을 수 없습니다."));
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", e.getMessage(), "message", "장소 저장 중 오류가 발생하였습니다."));
}
}


/**
* 사용자가 지금까지 저장한 검색 정보를 전부 최신순부터 가져옵니다.
*
* @param authentication 인증 정보
* @return 검색 기록 조회가 성공하면 200 OK와 결과 리스트를 반환합니다.
* 로그인을 하지 않고 시도 시 401 UNAUTHORIZED를 반환합니다.
* 사용자를 찾을 수 없는 경우 404 NOT_FOUND를 반환합니다.
* 기타 사유로 저장 실패 시 500 INTERNAL_SERVER_ERROR를 반환합니다.
*/
@GetMapping("")
public ResponseEntity<Object> getHistory(Authentication authentication) {
try {
if (authentication == null || !authentication.isAuthenticated()) {
return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
.body(Map.of("error", "UNAUTHORIZED", "message", "해당 서비스를 이용하기 위해서는 로그인이 필요합니다."));
}

String memberEmail = authentication.getName();
Member member = memberService.getMemberByEmail(memberEmail);
if (member == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(Map.of("error", "USER_NOT_FOUND", "message", "사용자를 찾을 수 없습니다."));
}

List<SearchHistoryResponseDtoV2> searchHistoryResponseDtos = searchHistoryService.getHistory(member);
return ResponseEntity.ok(searchHistoryResponseDtos);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
.body(Map.of("error", e.getMessage(), "message", "검색 기록 조회 중 오류가 발생하였습니다."));
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package com.solucitation.midpoint_backend.domain.history2.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class PlaceDtoV2 {
@NotBlank(message = "장소ID가 누락되었습니다.")
private String placeId; // 장소의 고유 ID

@NotBlank(message = "장소명이 누락되었습니다.")
private String placeName; // 장소의 이름

@NotBlank(message = "주소가 누락되었습니다.")
private String placeAddress; // 장소의 주소

private String imageUrl; // 장소 이미지
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.solucitation.midpoint_backend.domain.history2.dto;

import jakarta.validation.constraints.NotBlank;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.util.ArrayList;
import java.util.List;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SearchHistoryRequestDtoV2 {
@NotBlank
private String neighborhood;

private List<PlaceDtoV2> historyDto = new ArrayList<>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package com.solucitation.midpoint_backend.domain.history2.dto;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Getter
@NoArgsConstructor
@AllArgsConstructor
public class SearchHistoryResponseDtoV2 {
private String neighborhood;
private LocalDateTime serachTime;
private List<PlaceDtoV2> places = new ArrayList<>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
package com.solucitation.midpoint_backend.domain.history2.entity;

import jakarta.persistence.*;
import lombok.*;

@Entity
@Table(name = "place_info")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class PlaceInfoV2 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "place_info_id")
private Long id;

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

@ManyToOne
@JoinColumn(name = "search_history_id", nullable = false)
private SearchHistoryV2 searchHistory;

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

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

@Column(name="place_image_url", nullable = false)
private String imageUrl;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
package com.solucitation.midpoint_backend.domain.history2.entity;

import com.solucitation.midpoint_backend.domain.member.entity.Member;
import jakarta.persistence.*;
import lombok.*;
import org.hibernate.annotations.CreationTimestamp;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

import static jakarta.persistence.FetchType.LAZY;

@Entity
@Table(name = "search_history")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Builder
public class SearchHistoryV2 {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "search_history_id")
private Long id;

@Column(name="neighborhood", nullable = false)
private String neighborhood; // 동 정보 ex. 청파동

@ManyToOne(fetch = LAZY)
@JoinColumn(name = "member_id", nullable = false)
private Member member;

@CreationTimestamp
@Column(name = "search_date", nullable = false, updatable = false)
private LocalDateTime searchDate;

@OneToMany(mappedBy = "searchHistory", fetch = LAZY, cascade = CascadeType.ALL, orphanRemoval = true)
@Builder.Default
private List<PlaceInfoV2> placeList = new ArrayList<>();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package com.solucitation.midpoint_backend.domain.history2.repository;

import com.solucitation.midpoint_backend.domain.history2.entity.SearchHistoryV2;
import com.solucitation.midpoint_backend.domain.member.entity.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface SearchHistoryRepositoryV2 extends JpaRepository<SearchHistoryV2, Long> {
List<SearchHistoryV2> findByMemberOrderBySearchDateDesc(Member member);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,78 @@
package com.solucitation.midpoint_backend.domain.history2.service;

import com.solucitation.midpoint_backend.domain.history2.dto.PlaceDtoV2;
import com.solucitation.midpoint_backend.domain.history2.dto.SearchHistoryRequestDtoV2;
import com.solucitation.midpoint_backend.domain.history2.dto.SearchHistoryResponseDtoV2;
import com.solucitation.midpoint_backend.domain.history2.entity.PlaceInfoV2;
import com.solucitation.midpoint_backend.domain.history2.entity.SearchHistoryV2;
import com.solucitation.midpoint_backend.domain.history2.repository.SearchHistoryRepositoryV2;
import com.solucitation.midpoint_backend.domain.member.entity.Member;
import com.solucitation.midpoint_backend.domain.member.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;
import java.util.stream.Collectors;


@Service
@RequiredArgsConstructor
public class SearchHistoryServiceV2 {
@Value("${cloud.aws.s3.bucket}")
private String bucket;

private final String defaultImageUrl = String.format("https://%s.s3.%s.amazonaws.com/%s", bucket, "ap-northeast-2", "place_default_image.png");
private final SearchHistoryRepositoryV2 searchHistoryRepository;
private final MemberService memberService;

@Transactional
public void save(String neighborhood, String memberEmail, List<PlaceDtoV2> placeDtos) {
Member member = memberService.getMemberByEmail(memberEmail);

SearchHistoryV2 searchHistory = SearchHistoryV2.builder()
.member(member)
.neighborhood(neighborhood)
.build();

System.out.println("placeDtos.size() = " + placeDtos.size());
for (PlaceDtoV2 dto : placeDtos) {
String imageUrl = dto.getImageUrl();
if (imageUrl == null || imageUrl.trim().isEmpty()) { // 이미지 url이 없거나 공백
imageUrl = defaultImageUrl;
}

PlaceInfoV2 placeInfo = PlaceInfoV2.builder()
.name(dto.getPlaceName())
.placeId(dto.getPlaceId())
.address(dto.getPlaceAddress())
.imageUrl(imageUrl)
.searchHistory(searchHistory) // searchHistory 필드 설정
.build();
searchHistory.getPlaceList().add(placeInfo);
System.out.println("placeInfo = " + placeInfo);
}
searchHistoryRepository.save(searchHistory);
}

@Transactional(readOnly = true)
public List<SearchHistoryResponseDtoV2> getHistory(Member member) {
List<SearchHistoryV2> histories = searchHistoryRepository.findByMemberOrderBySearchDateDesc(member);

return histories.stream()
.map(history -> new SearchHistoryResponseDtoV2(
history.getNeighborhood(),
history.getSearchDate(),
history.getPlaceList().stream()
.map(placeInfo -> new PlaceDtoV2(
placeInfo.getPlaceId(),
placeInfo.getName(),
placeInfo.getAddress(),
placeInfo.getImageUrl()
))
.collect(Collectors.toList())
))
.collect(Collectors.toList());
}
}

0 comments on commit b79157e

Please sign in to comment.