diff --git a/.github/workflows/cd.yml b/.github/workflows/cd.yml index 9c478e18..6e6a0745 100644 --- a/.github/workflows/cd.yml +++ b/.github/workflows/cd.yml @@ -143,6 +143,7 @@ jobs: -e EATSSU_AWS_SECRET_KEY_PROD="${{ secrets.EATSSU_AWS_SECRET_KEY_PROD }}" \ -e EATSSU_SLACK_TOKEN="${{ secrets.EATSSU_SLACK_TOKEN }}" \ -e EATSSU_PROD_DOMAIN="${{ secrets.EATSSU_PROD_DOMAIN }}" \ + -e EATSSU_DEEPL_API_KEY="${{ secrets.EATSSU_DEEPL_API_KEY }}" \ ${{ secrets.DOCKER_REPO }}/eatssu-prod sudo docker image prune -f @@ -174,5 +175,6 @@ jobs: -e EATSSU_AWS_SECRET_KEY_DEV="${{ secrets.EATSSU_AWS_SECRET_KEY_DEV }}" \ -e EATSSU_SLACK_TOKEN="${{ secrets.EATSSU_SLACK_TOKEN }}" \ -e EATSSU_DEV_DOMAIN="${{ secrets.EATSSU_DEV_DOMAIN }}" \ + -e EATSSU_DEEPL_API_KEY="${{ secrets.EATSSU_DEEPL_API_KEY }}" \ ${{ secrets.DOCKER_REPO }}/eatssu-dev sudo docker image prune -f diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df5bc376..d053875b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,6 +31,7 @@ jobs: EATSSU_AWS_ACCESS_KEY_DEV: dummy-access-key EATSSU_AWS_SECRET_KEY_DEV: dummy-secret-key EATSSU_SLACK_TOKEN: dummy-slack-token + EATSSU_DEEPL_API_KEY: dummy-deepl-key steps: - uses: actions/checkout@v3 diff --git a/src/main/java/ssu/eatssu/domain/review/dto/ReviewTranslationResponse.java b/src/main/java/ssu/eatssu/domain/review/dto/ReviewTranslationResponse.java new file mode 100644 index 00000000..2e6e79a9 --- /dev/null +++ b/src/main/java/ssu/eatssu/domain/review/dto/ReviewTranslationResponse.java @@ -0,0 +1,11 @@ +package ssu.eatssu.domain.review.dto; + +import ssu.eatssu.domain.user.entity.Language; + +public record ReviewTranslationResponse( + Long reviewId, + Language language, + String translatedContent, + boolean cached +) { +} diff --git a/src/main/java/ssu/eatssu/domain/review/entity/ReviewTranslation.java b/src/main/java/ssu/eatssu/domain/review/entity/ReviewTranslation.java new file mode 100644 index 00000000..073a016f --- /dev/null +++ b/src/main/java/ssu/eatssu/domain/review/entity/ReviewTranslation.java @@ -0,0 +1,41 @@ +package ssu.eatssu.domain.review.entity; + +import jakarta.persistence.Entity; +import jakarta.persistence.EnumType; +import jakarta.persistence.Enumerated; +import jakarta.persistence.FetchType; +import jakarta.persistence.GeneratedValue; +import jakarta.persistence.GenerationType; +import jakarta.persistence.Id; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import lombok.AccessLevel; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; +import ssu.eatssu.domain.user.entity.BaseTimeEntity; +import ssu.eatssu.domain.user.entity.Language; + +@Entity +@Getter +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Builder +@AllArgsConstructor +public class ReviewTranslation extends BaseTimeEntity { + + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "review_id") + private Review review; + + @Enumerated(EnumType.STRING) + private Language language; + + private String translatedContent; + + private Integer charCount; +} diff --git a/src/main/java/ssu/eatssu/domain/review/infrastructure/DeepLConfig.java b/src/main/java/ssu/eatssu/domain/review/infrastructure/DeepLConfig.java new file mode 100644 index 00000000..849d4c37 --- /dev/null +++ b/src/main/java/ssu/eatssu/domain/review/infrastructure/DeepLConfig.java @@ -0,0 +1,37 @@ +package ssu.eatssu.domain.review.infrastructure; + +import org.apache.hc.client5.http.impl.classic.CloseableHttpClient; +import org.apache.hc.client5.http.impl.classic.HttpClientBuilder; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager; +import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.http.client.HttpComponentsClientHttpRequestFactory; +import org.springframework.web.client.RestTemplate; + +@Configuration +public class DeepLConfig { + + private static final int MAX_CONN_PER_ROUTE = 15; + private static final int MAX_CONN_TOTAL = 15; + private static final int CONNECT_TIMEOUT_MS = 2000; + private static final int CONNECTION_REQUEST_TIMEOUT_MS = 3000; + private static final int READ_TIMEOUT_MS = 3000; + + @Bean + public RestTemplate deeplRestTemplate() { + PoolingHttpClientConnectionManager connectionManager = PoolingHttpClientConnectionManagerBuilder.create() + .setMaxConnPerRoute(MAX_CONN_PER_ROUTE) + .setMaxConnTotal(MAX_CONN_TOTAL) + .build(); + CloseableHttpClient client = HttpClientBuilder.create() + .setConnectionManager(connectionManager) + .build(); + + HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory(client); + factory.setConnectTimeout(CONNECT_TIMEOUT_MS); + factory.setConnectionRequestTimeout(CONNECTION_REQUEST_TIMEOUT_MS); + factory.setReadTimeout(READ_TIMEOUT_MS); + return new RestTemplate(factory); + } +} diff --git a/src/main/java/ssu/eatssu/domain/review/infrastructure/DeepLTranslateResponse.java b/src/main/java/ssu/eatssu/domain/review/infrastructure/DeepLTranslateResponse.java new file mode 100644 index 00000000..2a6fab3c --- /dev/null +++ b/src/main/java/ssu/eatssu/domain/review/infrastructure/DeepLTranslateResponse.java @@ -0,0 +1,13 @@ +package ssu.eatssu.domain.review.infrastructure; + +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.List; + +public record DeepLTranslateResponse(List translations) { + + public record Translation( + @JsonProperty("detected_source_language") String detectedSourceLanguage, + String text + ) { + } +} diff --git a/src/main/java/ssu/eatssu/domain/review/infrastructure/DeepLTranslationClient.java b/src/main/java/ssu/eatssu/domain/review/infrastructure/DeepLTranslationClient.java new file mode 100644 index 00000000..40a00c39 --- /dev/null +++ b/src/main/java/ssu/eatssu/domain/review/infrastructure/DeepLTranslationClient.java @@ -0,0 +1,68 @@ +package ssu.eatssu.domain.review.infrastructure; + +import lombok.RequiredArgsConstructor; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Component; +import org.springframework.util.LinkedMultiValueMap; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.HttpClientErrorException; +import org.springframework.web.client.HttpServerErrorException; +import org.springframework.web.client.ResourceAccessException; +import org.springframework.web.client.RestClientException; +import org.springframework.web.client.RestTemplate; +import ssu.eatssu.domain.user.entity.Language; +import ssu.eatssu.global.handler.response.BaseException; + +import static ssu.eatssu.global.handler.response.BaseResponseStatus.TRANSLATION_FAILED; +import static ssu.eatssu.global.handler.response.BaseResponseStatus.TRANSLATION_QUOTA_EXCEEDED; +import static ssu.eatssu.global.handler.response.BaseResponseStatus.TRANSLATION_TIMEOUT; + +@Component +@RequiredArgsConstructor +public class DeepLTranslationClient { + + private static final int QUOTA_EXCEEDED_STATUS = 456; + + private final RestTemplate deeplRestTemplate; + + @Value("${deepl.api-key}") + private String apiKey; + + @Value("${deepl.base-url:https://api-free.deepl.com}") + private String baseUrl; + + public String translate(String text, Language targetLanguage) { + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); + headers.set("Authorization", "DeepL-Auth-Key " + apiKey); + + MultiValueMap body = new LinkedMultiValueMap<>(); + body.add("text", text); + body.add("target_lang", targetLanguage.name()); + + HttpEntity> request = new HttpEntity<>(body, headers); + + try { + DeepLTranslateResponse response = deeplRestTemplate.postForObject( + baseUrl + "/v2/translate", request, DeepLTranslateResponse.class); + + if (response == null || response.translations().isEmpty()) { + throw new BaseException(TRANSLATION_FAILED); + } + return response.translations().get(0).text(); + } catch (HttpClientErrorException | HttpServerErrorException e) { + int statusCode = e.getStatusCode().value(); + if (statusCode == 429 || statusCode == QUOTA_EXCEEDED_STATUS) { + throw new BaseException(TRANSLATION_QUOTA_EXCEEDED); + } + throw new BaseException(TRANSLATION_FAILED); + } catch (ResourceAccessException e) { + throw new BaseException(TRANSLATION_TIMEOUT); + } catch (RestClientException e) { + throw new BaseException(TRANSLATION_FAILED); + } + } +} diff --git a/src/main/java/ssu/eatssu/domain/review/presentation/ReviewControllerV2.java b/src/main/java/ssu/eatssu/domain/review/presentation/ReviewControllerV2.java index 320c8e70..c19382e2 100644 --- a/src/main/java/ssu/eatssu/domain/review/presentation/ReviewControllerV2.java +++ b/src/main/java/ssu/eatssu/domain/review/presentation/ReviewControllerV2.java @@ -34,11 +34,14 @@ import ssu.eatssu.domain.review.dto.MenuReviewsV2Response; import ssu.eatssu.domain.review.dto.RestaurantReviewResponse; import ssu.eatssu.domain.review.dto.ReviewDetail; +import ssu.eatssu.domain.review.dto.ReviewTranslationResponse; import ssu.eatssu.domain.review.dto.UpdateMealReviewRequest; import ssu.eatssu.domain.review.dto.ValidMenuForViewResponse; import ssu.eatssu.domain.review.service.ReviewServiceV2; +import ssu.eatssu.domain.review.service.ReviewTranslationService; import ssu.eatssu.domain.slice.dto.SliceResponse; import ssu.eatssu.domain.user.dto.MyMealReviewResponse; +import ssu.eatssu.domain.user.entity.Language; import ssu.eatssu.global.handler.response.BaseResponse; @RestController @@ -47,6 +50,7 @@ @Tag(name = "Review V2", description = "리뷰 V2 API") public class ReviewControllerV2 { private final ReviewServiceV2 reviewServiceV2; + private final ReviewTranslationService reviewTranslationService; @Operation(summary = "meal(식단)에 대한 리뷰 작성", description = "리뷰를 작성하는 API 입니다.") @ApiResponses(value = { @@ -139,6 +143,25 @@ public BaseResponse deleteReview( return BaseResponse.success(); } + @Operation(summary = "리뷰 번역", description = """ + 리뷰 내용을 DeepL로 번역하는 API 입니다.

+ 같은 리뷰/언어 조합은 캐시된 결과를 반환합니다.

+ 현재는 language=EN만 지원합니다. + """) + @ApiResponses(value = { + @ApiResponse(responseCode = "200", description = "리뷰 번역 성공"), + @ApiResponse(responseCode = "400", description = "지원하지 않는 언어이거나 번역할 내용이 없음", content = @Content(schema = @Schema(implementation = BaseResponse.class))), + @ApiResponse(responseCode = "404", description = "존재하지 않는 리뷰", content = @Content(schema = @Schema(implementation = BaseResponse.class))), + @ApiResponse(responseCode = "429", description = "번역 API 사용량 한도 초과", content = @Content(schema = @Schema(implementation = BaseResponse.class))), + @ApiResponse(responseCode = "503", description = "번역 시간 초과 또는 실패", content = @Content(schema = @Schema(implementation = BaseResponse.class))) + }) + @PostMapping("/{reviewId}/translate") + public BaseResponse translateReview( + @Parameter(description = "reviewId") @PathVariable("reviewId") Long reviewId, + @Parameter(description = "번역 대상 언어(현재 EN만 지원)") @RequestParam Language language) { + return BaseResponse.success(reviewTranslationService.translateReview(reviewId, language)); + } + @Operation(summary = "식단(변동 메뉴) 리뷰 정보 조회 V2(메뉴명, 평점 등등) [인증 토큰 필요 X]", description = """ 식단 리뷰 정보를 조회하는 API 입니다.

메뉴명 리스트, 리뷰 수, 메인 평점, 좋아요 개수, 싫어요 개수, 각 평점의 개수를 조회합니다. diff --git a/src/main/java/ssu/eatssu/domain/review/repository/ReviewTranslationRepository.java b/src/main/java/ssu/eatssu/domain/review/repository/ReviewTranslationRepository.java new file mode 100644 index 00000000..2209b497 --- /dev/null +++ b/src/main/java/ssu/eatssu/domain/review/repository/ReviewTranslationRepository.java @@ -0,0 +1,13 @@ +package ssu.eatssu.domain.review.repository; + +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import ssu.eatssu.domain.review.entity.ReviewTranslation; +import ssu.eatssu.domain.user.entity.Language; + +public interface ReviewTranslationRepository extends JpaRepository { + + Optional findByReview_IdAndLanguage(Long reviewId, Language language); + + void deleteAllByReview_Id(Long reviewId); +} diff --git a/src/main/java/ssu/eatssu/domain/review/service/ReviewService.java b/src/main/java/ssu/eatssu/domain/review/service/ReviewService.java index 03337dd9..87836a3f 100644 --- a/src/main/java/ssu/eatssu/domain/review/service/ReviewService.java +++ b/src/main/java/ssu/eatssu/domain/review/service/ReviewService.java @@ -23,6 +23,7 @@ import ssu.eatssu.domain.review.entity.ReviewImage; import ssu.eatssu.domain.review.repository.ReviewImageRepository; import ssu.eatssu.domain.review.repository.ReviewRepository; +import ssu.eatssu.domain.review.repository.ReviewTranslationRepository; import ssu.eatssu.domain.user.entity.User; import ssu.eatssu.domain.user.repository.UserRepository; import ssu.eatssu.global.handler.response.BaseException; @@ -30,6 +31,7 @@ import java.io.IOException; import java.util.List; +import java.util.Objects; import static ssu.eatssu.global.handler.response.BaseResponseStatus.FAIL_IMAGE_UPLOAD; import static ssu.eatssu.global.handler.response.BaseResponseStatus.NOT_FOUND_MEAL; @@ -47,6 +49,7 @@ public class ReviewService { private final UserRepository userRepository; private final ReviewRepository reviewRepository; private final ReviewImageRepository reviewImageRepository; + private final ReviewTranslationRepository reviewTranslationRepository; private final MenuRepository menuRepository; private final MealRepository mealRepository; private final RatingCalculator ratingCalculator; @@ -132,7 +135,12 @@ public void updateReview(CustomUserDetails userDetails, Long reviewId, throw new BaseException(REVIEW_PERMISSION_DENIED); } + String oldContent = review.getContent(); review.update(request.content(), request.mainRating(),request.amountRating(),request.tasteRating()); + + if (!Objects.equals(oldContent, request.content())) { + reviewTranslationRepository.deleteAllByReview_Id(reviewId); + } } public void deleteReview(CustomUserDetails userDetails, Long reviewId) { diff --git a/src/main/java/ssu/eatssu/domain/review/service/ReviewServiceV2.java b/src/main/java/ssu/eatssu/domain/review/service/ReviewServiceV2.java index 25671410..4e471d48 100644 --- a/src/main/java/ssu/eatssu/domain/review/service/ReviewServiceV2.java +++ b/src/main/java/ssu/eatssu/domain/review/service/ReviewServiceV2.java @@ -32,6 +32,7 @@ import ssu.eatssu.domain.review.dto.ValidMenuForViewResponse; import ssu.eatssu.domain.review.entity.Review; import ssu.eatssu.domain.review.repository.ReviewRepository; +import ssu.eatssu.domain.review.repository.ReviewTranslationRepository; import ssu.eatssu.domain.review.utils.MenuFilterUtil; import ssu.eatssu.domain.slice.dto.SliceResponse; import ssu.eatssu.domain.user.dto.MyMealReviewResponse; @@ -60,6 +61,7 @@ public class ReviewServiceV2 { private final UserRepository userRepository; private final ReviewRepository reviewRepository; + private final ReviewTranslationRepository reviewTranslationRepository; private final MenuRepository menuRepository; private final MealRepository mealRepository; private final MealMenuRepository mealMenuRepository; @@ -415,9 +417,14 @@ public void updateReview(CustomUserDetails userDetails, Long reviewId, UpdateMea }, menuLike -> Boolean.TRUE.equals(menuLike.getIsLike()))); + String oldContent = review.getContent(); review.update(request.getContent(), request.getRating(), menuLikes); reviewRepository.save(review); + if (!Objects.equals(oldContent, request.getContent())) { + reviewTranslationRepository.deleteAllByReview_Id(reviewId); + } + eventPublisher.publishEvent(LogEvent.of( String.format("Review updated: reviewId=%d, userId=%d, newRating=%d", review.getId(), user.getId(), request.getRating()) diff --git a/src/main/java/ssu/eatssu/domain/review/service/ReviewTranslationService.java b/src/main/java/ssu/eatssu/domain/review/service/ReviewTranslationService.java new file mode 100644 index 00000000..eccdfe38 --- /dev/null +++ b/src/main/java/ssu/eatssu/domain/review/service/ReviewTranslationService.java @@ -0,0 +1,75 @@ +package ssu.eatssu.domain.review.service; + +import java.util.Optional; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; +import org.springframework.dao.DataIntegrityViolationException; +import org.springframework.stereotype.Service; +import org.springframework.transaction.support.TransactionTemplate; +import ssu.eatssu.domain.review.dto.ReviewTranslationResponse; +import ssu.eatssu.domain.review.entity.Review; +import ssu.eatssu.domain.review.entity.ReviewTranslation; +import ssu.eatssu.domain.review.infrastructure.DeepLTranslationClient; +import ssu.eatssu.domain.review.repository.ReviewRepository; +import ssu.eatssu.domain.review.repository.ReviewTranslationRepository; +import ssu.eatssu.domain.user.entity.Language; +import ssu.eatssu.global.handler.response.BaseException; + +import static ssu.eatssu.global.handler.response.BaseResponseStatus.FAILED_VALIDATION; +import static ssu.eatssu.global.handler.response.BaseResponseStatus.NOT_FOUND_REVIEW; + +@Slf4j +@RequiredArgsConstructor +@Service +public class ReviewTranslationService { + + private final ReviewRepository reviewRepository; + private final ReviewTranslationRepository reviewTranslationRepository; + private final DeepLTranslationClient deeplTranslationClient; + private final TransactionTemplate transactionTemplate; + + public ReviewTranslationResponse translateReview(Long reviewId, Language language) { + if (language != Language.EN) { + throw new BaseException(FAILED_VALIDATION); + } + + Review review = reviewRepository.findById(reviewId) + .orElseThrow(() -> new BaseException(NOT_FOUND_REVIEW)); + + Optional cached = reviewTranslationRepository.findByReview_IdAndLanguage(reviewId, + language); + if (cached.isPresent()) { + ReviewTranslation translation = cached.get(); + log.info("[리뷰 번역] reviewId={} language={} charCount={} cached=true", reviewId, language, + translation.getCharCount()); + return new ReviewTranslationResponse(reviewId, language, translation.getTranslatedContent(), true); + } + + String content = review.getContent(); + if (content == null || content.isBlank()) { + throw new BaseException(FAILED_VALIDATION); + } + + String translatedContent = deeplTranslationClient.translate(content, language); + int charCount = content.length(); + + saveTranslation(review, language, translatedContent, charCount); + + log.info("[리뷰 번역] reviewId={} language={} charCount={} cached=false", reviewId, language, charCount); + return new ReviewTranslationResponse(reviewId, language, translatedContent, false); + } + + private void saveTranslation(Review review, Language language, String translatedContent, int charCount) { + try { + transactionTemplate.executeWithoutResult(status -> + reviewTranslationRepository.save(ReviewTranslation.builder() + .review(review) + .language(language) + .translatedContent(translatedContent) + .charCount(charCount) + .build())); + } catch (DataIntegrityViolationException e) { + log.warn("[리뷰 번역] 캐시 중복 저장 감지 reviewId={} language={}", review.getId(), language); + } + } +} diff --git a/src/main/java/ssu/eatssu/global/handler/response/BaseResponseStatus.java b/src/main/java/ssu/eatssu/global/handler/response/BaseResponseStatus.java index dd264ff5..1ba88cb3 100644 --- a/src/main/java/ssu/eatssu/global/handler/response/BaseResponseStatus.java +++ b/src/main/java/ssu/eatssu/global/handler/response/BaseResponseStatus.java @@ -96,6 +96,11 @@ public enum BaseResponseStatus { DUPLICATE_EMAIL(false, HttpStatus.CONFLICT, 40901, "중복된 이메일입니다."), DUPLICATE_NICKNAME(false, HttpStatus.CONFLICT, 40902, "중복된 닉네임입니다."), + /** + * 429 TOO_MANY_REQUESTS 요청 한도 초과 + */ + TRANSLATION_QUOTA_EXCEEDED(false, HttpStatus.TOO_MANY_REQUESTS, 42901, "번역 API 사용량 한도를 초과했습니다."), + /** * 415 UNSUPPORTED_MEDIA_TYPE 지원하지 않는 content type */ @@ -111,7 +116,9 @@ public enum BaseResponseStatus { * 503 SERVICE_UNAVAILABLE 서버 내부 에러 */ SERVICE_UNAVAILABLE(false, HttpStatus.SERVICE_UNAVAILABLE, 503, "현재 서비스가 불가능한 상태입니다."), - INTERNAL_SERVER_TIME_OUT(false, HttpStatus.SERVICE_UNAVAILABLE, 503, "서버에서 시간초과가 발생했습니다."); + INTERNAL_SERVER_TIME_OUT(false, HttpStatus.SERVICE_UNAVAILABLE, 503, "서버에서 시간초과가 발생했습니다."), + TRANSLATION_TIMEOUT(false, HttpStatus.SERVICE_UNAVAILABLE, 50301, "번역 요청 시간이 초과되었습니다."), + TRANSLATION_FAILED(false, HttpStatus.SERVICE_UNAVAILABLE, 50302, "번역에 실패했습니다."); private final boolean isSuccess; private final HttpStatus httpStatus; diff --git a/src/main/resources/application-dev.yml b/src/main/resources/application-dev.yml index 017f7054..e97dde3e 100644 --- a/src/main/resources/application-dev.yml +++ b/src/main/resources/application-dev.yml @@ -68,6 +68,11 @@ cloud: slack: token: ${EATSSU_SLACK_TOKEN} +#DeepL +deepl: + api-key: ${EATSSU_DEEPL_API_KEY} + base-url: https://api-free.deepl.com + #Swagger swagger: url: ${EATSSU_DEV_DOMAIN} diff --git a/src/main/resources/application-prod.yml b/src/main/resources/application-prod.yml index e37f5096..6b38fd4d 100644 --- a/src/main/resources/application-prod.yml +++ b/src/main/resources/application-prod.yml @@ -69,6 +69,11 @@ cloud: slack: token: ${EATSSU_SLACK_TOKEN} +#DeepL +deepl: + api-key: ${EATSSU_DEEPL_API_KEY} + base-url: https://api-free.deepl.com + #Swagger swagger: url: ${EATSSU_PROD_DOMAIN} diff --git a/src/main/resources/db/migration/V18__create_review_translation_table.sql b/src/main/resources/db/migration/V18__create_review_translation_table.sql new file mode 100644 index 00000000..edbc6071 --- /dev/null +++ b/src/main/resources/db/migration/V18__create_review_translation_table.sql @@ -0,0 +1,13 @@ +CREATE TABLE review_translation +( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + review_id BIGINT NOT NULL, + language VARCHAR(10) NOT NULL, + translated_content VARCHAR(600) NOT NULL, + char_count INT NOT NULL, + created_date DATETIME(6) NOT NULL, + modified_date DATETIME(6) NOT NULL, + CONSTRAINT uk_review_translation_review_lang UNIQUE (review_id, language), + CONSTRAINT fk_review_translation_review + FOREIGN KEY (review_id) REFERENCES review (review_id) ON DELETE CASCADE +); diff --git a/src/test/resources/application-test.yml b/src/test/resources/application-test.yml index d2e73163..3c206e1b 100644 --- a/src/test/resources/application-test.yml +++ b/src/test/resources/application-test.yml @@ -47,6 +47,11 @@ cloud: slack: token: ${EATSSU_SLACK_TOKEN} +#DeepL +deepl: + api-key: ${EATSSU_DEEPL_API_KEY} + base-url: https://api-free.deepl.com + swagger: url: http://localhost:9000 description: Test Server Swagger API