Skip to content

Commit 350db6a

Browse files
authored
Merge pull request #55 from JocketDan/refactor/review-translate
Refactor: 리뷰 번역 로직 개선 및 캐싱 적용
2 parents b2f4e1c + 894af7d commit 350db6a

11 files changed

Lines changed: 86 additions & 67 deletions

File tree

review/src/main/java/com/jocketdan/review/controller/ReviewController.java

Lines changed: 13 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -32,15 +32,16 @@ public class ReviewController {
3232
@Operation(
3333
summary = "리뷰 생성 API",
3434
description = """
35-
특정 콘텐츠(`placeId`)에 대한 리뷰와 이미지를 등록합니다.
35+
특정 콘텐츠(`contentId`)에 대한 리뷰와 이미지를 등록합니다.
3636
- 이미지는 **최대 10장까지 업로드**할 수 있습니다.
3737
- 리뷰 정보(`dto`)는 **문자열(JSON 형태)**로 전달해야 합니다. (Swagger 테스트 시 주의)
3838
3939
dto 예시 요청 형식:
4040
```
4141
{
4242
"content": "리뷰 내용",
43-
"rating": 4
43+
"rating": 4 ,
44+
"lang" : "ko"
4445
}
4546
```
4647
"""
@@ -62,7 +63,7 @@ public ResponseEntity<ReviewResponseDTO> createReview(
6263
@Operation(
6364
summary = "리뷰 조회 API",
6465
description = """
65-
특정 콘텐츠(`placeId`)에 등록된 리뷰 목록을 조회합니다.
66+
특정 콘텐츠(`contentId`)에 등록된 리뷰 목록을 조회합니다.
6667
리뷰는 선택한 언어(`lang`)로 번역되어 반환되며, 정렬 기준(`arrange`)을 지정할 수 있습니다.
6768
6869
### 언어 설정 (`lang`)
@@ -78,7 +79,7 @@ public ResponseEntity<ReviewResponseDTO> createReview(
7879
"""
7980
)
8081
@GetMapping("/{contentId}")
81-
public ResponseEntity<ContentReviewResponseDTO> getReviewsByPlaceId(
82+
public ResponseEntity<ContentReviewResponseDTO> getReviewsByContentId(
8283
@PathVariable String contentId,
8384
@RequestParam(defaultValue = "10") int numOfRows,
8485
@RequestParam(defaultValue = "1") int pageNo,
@@ -88,7 +89,7 @@ public ResponseEntity<ContentReviewResponseDTO> getReviewsByPlaceId(
8889
Sort sort = createReviewSort(arrange);
8990
Pageable pageable = PageRequest.of(pageNo - 1, numOfRows, sort);
9091

91-
ContentReviewResponseDTO response = reviewService.readReviews(contentId, lang, pageable);
92+
ContentReviewResponseDTO response = reviewService.getReviewsByContentId(contentId, lang, pageable);
9293
return ResponseEntity.ok(response);
9394
}
9495

@@ -151,8 +152,11 @@ public ResponseEntity<Void> deleteReview(@PathVariable Long reviewId,
151152
"""
152153
)
153154
@GetMapping("/reviews/{reviewId}")
154-
public ResponseEntity<ReviewWithPlaceResponseDTO> getReview(@PathVariable Long reviewId) {
155-
return ResponseEntity.ok(reviewService.getReview(reviewId));
155+
public ResponseEntity<ReviewWithPlaceResponseDTO> getReview(
156+
@PathVariable Long reviewId,
157+
@RequestParam(defaultValue = "ko") String lang) {
158+
159+
return ResponseEntity.ok(reviewService.getReview(reviewId, lang));
156160
}
157161

158162
// 내가 쓴 리뷰 관리 ========================================
@@ -172,13 +176,14 @@ public ResponseEntity<PaginatedResponseDTO<ReviewWithPlaceResponseDTO>> getMyRev
172176
@RequestParam(defaultValue = "10") int numOfRows,
173177
@RequestParam(defaultValue = "1") int pageNo,
174178
@RequestParam(defaultValue = "C") String arrange,
179+
@RequestParam(defaultValue = "ko") String lang,
175180
Authentication authentication) {
176181

177182
Sort sort = createReviewSort(arrange);
178183
Pageable pageable = PageRequest.of(pageNo - 1, numOfRows, sort);
179184

180185
String email = authentication.getName();
181-
Page<ReviewWithPlaceResponseDTO> reviews = reviewService.getMyReviews(email, pageable);
186+
Page<ReviewWithPlaceResponseDTO> reviews = reviewService.getMyReviews(email, lang, pageable);
182187
return ResponseEntity.ok(PaginatedResponseDTO.of(reviews));
183188
}
184189

review/src/main/java/com/jocketdan/review/dto/ReviewRequestDTO.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,14 @@
1515
@NoArgsConstructor
1616
public class ReviewRequestDTO {
1717
private String content;
18+
1819
@Schema(description = "별점 (1-5점)", defaultValue = "0", example = "5")
1920
private int rating;
2021

22+
private String lang = "ko";
23+
2124
public Review toEntity(String contentId, String email, List<String> imageUrls) {
22-
Review review = new Review(email, contentId, this.content, this.rating);
25+
Review review = new Review(email, contentId, this.content, this.rating, this.lang);
2326

2427
if (imageUrls != null && !imageUrls.isEmpty()) {
2528
List<ReviewImage> reviewImages = imageUrls.stream()

review/src/main/java/com/jocketdan/review/dto/ReviewResponseDTO.java

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,6 @@
66
import lombok.experimental.FieldDefaults;
77

88
import java.util.List;
9-
import java.util.stream.Collectors;
109

1110
@Setter
1211
@Getter
@@ -18,6 +17,7 @@ public class ReviewResponseDTO {
1817
Long id;
1918
String email;
2019
String content;
20+
String originLang;
2121
int rating;
2222
List<String> imageUrls;
2323

@@ -26,6 +26,13 @@ public static ReviewResponseDTO from(Review review) {
2626
.map(ReviewImage::getImageUrl)
2727
.toList();
2828

29-
return new ReviewResponseDTO(review.getId(), review.getEmail(), review.getContent(), review.getRating() , urls);
29+
return new ReviewResponseDTO(
30+
review.getId(),
31+
review.getEmail(),
32+
review.getContent(),
33+
review.getOriginLang(),
34+
review.getRating(),
35+
urls
36+
);
3037
}
3138
}

review/src/main/java/com/jocketdan/review/dto/ReviewWithPlaceResponseDTO.java

Lines changed: 10 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
package com.jocketdan.review.dto;
22

33
import com.jocketdan.review.entity.OpenApiInfoCache;
4-
import com.jocketdan.review.entity.Review;
5-
import com.jocketdan.review.entity.ReviewImage;
64
import lombok.*;
75
import lombok.experimental.FieldDefaults;
86

@@ -30,17 +28,15 @@ public class ReviewWithPlaceResponseDTO {
3028
String areaCode;
3129
String sigunguCode;
3230

33-
public static ReviewWithPlaceResponseDTO from(Review review, OpenApiInfoCache placeInfo) {
34-
List<String> urls = review.getImages().stream()
35-
.map(ReviewImage::getImageUrl)
36-
.toList();
31+
public static ReviewWithPlaceResponseDTO from(ReviewResponseDTO reviewDto, OpenApiInfoCache placeInfo) {
32+
List<String> urls = reviewDto.getImageUrls();
3733

3834
if (placeInfo == null) {
3935
return new ReviewWithPlaceResponseDTO(
40-
review.getId(),
41-
review.getEmail(),
42-
review.getContent(),
43-
review.getRating(),
36+
reviewDto.getId(),
37+
reviewDto.getEmail(),
38+
reviewDto.getContent(),
39+
reviewDto.getRating(),
4440
urls,
4541
"[삭제되거나 알 수 없는 장소]",
4642
"[삭제되거나 알 수 없는 장소]",
@@ -54,10 +50,10 @@ public static ReviewWithPlaceResponseDTO from(Review review, OpenApiInfoCache pl
5450
}
5551

5652
return new ReviewWithPlaceResponseDTO(
57-
review.getId(),
58-
review.getEmail(),
59-
review.getContent(),
60-
review.getRating(),
53+
reviewDto.getId(),
54+
reviewDto.getEmail(),
55+
reviewDto.getContent(),
56+
reviewDto.getRating(),
6157
urls,
6258
placeInfo.getContentId(),
6359
placeInfo.getTitle(),

review/src/main/java/com/jocketdan/review/entity/Review.java

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,14 +28,18 @@ public class Review {
2828
int rating;
2929
boolean deleted = false;
3030

31+
@Column(nullable = false)
32+
String originLang;
33+
3134
@OneToMany(mappedBy = "review", cascade = CascadeType.ALL, orphanRemoval = true)
3235
List<ReviewImage> images = new ArrayList<>();
3336

34-
public Review(String email, String contentId, String content, int rating) {
37+
public Review(String email, String contentId, String content, int rating, String originLang) {
3538
this.email = email;
3639
this.contentId = contentId;
3740
this.content = content;
3841
this.rating = rating;
42+
this.originLang = originLang;
3943
}
4044

4145
public void setImages(List<ReviewImage> images) {

review/src/main/java/com/jocketdan/review/repository/ReviewRepository.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
public interface ReviewRepository extends JpaRepository<Review, Long> {
1313
Page<Review> findByContentIdAndDeletedFalse(String contentId, Pageable pageable);
1414

15-
@Query("SELECT AVG(r.rating) FROM Review r WHERE r.contentId = :placeId AND r.deleted = false")
15+
@Query("SELECT AVG(r.rating) FROM Review r WHERE r.contentId = :contentId AND r.deleted = false")
1616
Optional<Double> findAverageRatingByContentId(@Param("contentId") String contentId);
1717

1818
@Query("SELECT r FROM Review r WHERE EXISTS (SELECT rr FROM ReviewReport rr WHERE rr.review = r)")

review/src/main/java/com/jocketdan/review/service/ReviewService.java

Lines changed: 22 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ public ReviewResponseDTO createReview(ReviewRequestDTO dto, List<MultipartFile>
6363
}
6464

6565
@Transactional(readOnly = true)
66-
public ContentReviewResponseDTO readReviews(String contentId, String lang, Pageable pageable) {
66+
public ContentReviewResponseDTO getReviewsByContentId(String contentId, String lang, Pageable pageable) {
6767
Page<Review> reviewPage = reviewRepository.findByContentIdAndDeletedFalse(contentId, pageable);
6868

6969
double rawAverage = reviewRepository.findAverageRatingByContentId(contentId)
@@ -78,14 +78,7 @@ public ContentReviewResponseDTO readReviews(String contentId, String lang, Pagea
7878
);
7979
}
8080

81-
Page<ReviewResponseDTO> reviewDtoPage = reviewPage.map(review -> {
82-
ReviewResponseDTO dto = ReviewResponseDTO.from(review);
83-
if (lang != null && !"ko".equalsIgnoreCase(lang)) {
84-
String translated = translationService.translateText(review.getContent(), lang);
85-
dto.setContent(translated);
86-
}
87-
return dto;
88-
});
81+
Page<ReviewResponseDTO> reviewDtoPage = reviewPage.map(review -> toReviewResponseDTO(review, lang));
8982

9083
return new ContentReviewResponseDTO(reviewDtoPage, contentId, averageRating);
9184
}
@@ -139,46 +132,40 @@ public void deleteReview(Long reviewId, String email) {
139132
}
140133

141134
@Transactional(readOnly = true)
142-
public ReviewWithPlaceResponseDTO getReview(Long reviewId) {
135+
public ReviewWithPlaceResponseDTO getReview(Long reviewId, String lang) {
143136
Review review = reviewRepository.findById(reviewId)
144137
.orElseThrow(() -> new CustomException(ExceptionType.REVIEW_NOT_FOUND));
145138

146139
OpenApiInfoCache placeInfo = openApiInfoCacheRepository
147140
.findById(review.getContentId())
148141
.orElse(null);
149142

150-
return ReviewWithPlaceResponseDTO.from(review, placeInfo);
143+
ReviewResponseDTO reviewDto = toReviewResponseDTO(review, lang);
144+
145+
return ReviewWithPlaceResponseDTO.from(reviewDto, placeInfo);
151146
}
152147

153148
@Transactional(readOnly = true)
154-
public Page<ReviewWithPlaceResponseDTO> getMyReviews(String email, Pageable pageable) {
149+
public Page<ReviewWithPlaceResponseDTO> getMyReviews(String email, String lang, Pageable pageable) {
155150
Page<Review> reviews = reviewRepository.findAllByEmailAndDeletedFalse(email, pageable);
156151

157152
List<String> contentIds = reviews.getContent().stream()
158153
.map(Review::getContentId)
159154
.distinct()
160155
.collect(Collectors.toList());
161156

162-
log.warn("리뷰 contentIds = {}", contentIds);
163-
164157
Map<String, OpenApiInfoCache> placeInfoMap = openApiInfoCacheRepository.findByContentIdIn(contentIds)
165158
.stream()
166-
.peek(info -> log.warn(" [DB 조회 결과] contentId={}, title={}", info.getContentId(), info.getTitle()))
167159
.collect(Collectors.toMap(
168160
OpenApiInfoCache::getContentId,
169161
placeInfo -> placeInfo
170162
));
171163

172-
placeInfoMap.forEach((key, value) -> {
173-
log.warn("key={}, value.title={}, value.addr1={}", key, value.getTitle(), value.getAddr1());
174-
});
175-
176164
return reviews.map(review -> {
177165
OpenApiInfoCache placeInfo = placeInfoMap.get(review.getContentId());
178-
if (placeInfo == null) {
179-
log.warn("[NULL] placeInfo 없음: review.contentId={}", review.getContentId());
180-
}
181-
return ReviewWithPlaceResponseDTO.from(review, placeInfo);
166+
ReviewResponseDTO reviewDto = toReviewResponseDTO(review, lang);
167+
168+
return ReviewWithPlaceResponseDTO.from(reviewDto, placeInfo);
182169
});
183170
}
184171

@@ -187,4 +174,16 @@ public Page<ReviewResponseDTO> getReportReviews(Pageable pageable) {
187174
Page<Review> reviews = reviewRepository.findReportedReviews(pageable);
188175
return reviews.map(ReviewResponseDTO::from);
189176
}
177+
178+
private ReviewResponseDTO toReviewResponseDTO(Review review, String targetLang) {
179+
ReviewResponseDTO dto = ReviewResponseDTO.from(review);
180+
181+
if (!targetLang.equalsIgnoreCase(review.getOriginLang())) {
182+
dto.setContent(
183+
translationService.translateText(review.getContent(), targetLang)
184+
);
185+
}
186+
187+
return dto;
188+
}
190189
}

review/src/main/java/com/jocketdan/review/service/TranslationService.java

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,11 @@
33
import com.google.cloud.translate.Translate;
44
import com.google.cloud.translate.TranslateException;
55
import com.google.cloud.translate.Translation;
6-
import com.jocketdan.review.exception.CustomException;
7-
import com.jocketdan.review.exception.ExceptionType;
86
import lombok.AccessLevel;
97
import lombok.RequiredArgsConstructor;
108
import lombok.experimental.FieldDefaults;
119
import lombok.extern.slf4j.Slf4j;
10+
import org.springframework.cache.annotation.Cacheable;
1211
import org.springframework.stereotype.Service;
1312

1413
@Slf4j
@@ -19,6 +18,7 @@ public class TranslationService {
1918

2019
Translate translate;
2120

21+
@Cacheable(value = "translations", key = "#text + '_' + #targetLang")
2222
public String translateText(String text, String targetLang) {
2323
try {
2424
Translation translation = translate.translate(
@@ -29,7 +29,7 @@ public String translateText(String text, String targetLang) {
2929
return translation.getTranslatedText();
3030
} catch (TranslateException e) {
3131
log.error("Google 번역 API 연동 실패. targetLang={}", targetLang, e);
32-
throw new CustomException(ExceptionType.TRANSLATION_FAILED);
32+
return text;
3333
}
3434
}
3535
}

review/src/main/resources/application.yaml

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,11 @@ spring:
55
application:
66
name: review
77

8+
cache:
9+
type: caffeine
10+
caffeine:
11+
spec: maximumSize=10000,expireAfterWrite=7d
12+
813
servlet:
914
multipart:
1015
max-file-size: 15MB
@@ -49,7 +54,7 @@ springdoc:
4954
google:
5055
cloud:
5156
project-id: "JocketDan"
52-
key-path: "classpath:jocketdan-030b962979f8.json"
57+
key-path: "classpath:jocketdan-ab0120fcfd2d.json"
5358

5459
ncloud:
5560
storage:

review/src/main/resources/jocketdan-030b962979f8.json

Lines changed: 0 additions & 13 deletions
This file was deleted.

0 commit comments

Comments
 (0)