From 418382c405d329ff70379f52155124c7da35e8aa Mon Sep 17 00:00:00 2001 From: Kimprodp Date: Wed, 20 Nov 2024 23:32:03 +0900 Subject: [PATCH 1/8] =?UTF-8?q?[BE]=20refactor:=202=EC=B0=A8=20=EC=A0=95?= =?UTF-8?q?=EB=A0=AC=20=EA=B8=B0=EC=A4=80=20=EC=B6=94=EA=B0=80=20(#972)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 2차 정렬 기준 추가 * refactor: 2차 정렬 기준 추가 --- .../main/java/reviewme/review/repository/AnswerRepository.java | 2 +- .../main/java/reviewme/review/repository/ReviewRepository.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/backend/src/main/java/reviewme/review/repository/AnswerRepository.java b/backend/src/main/java/reviewme/review/repository/AnswerRepository.java index ea793623a..5b18ab9f2 100644 --- a/backend/src/main/java/reviewme/review/repository/AnswerRepository.java +++ b/backend/src/main/java/reviewme/review/repository/AnswerRepository.java @@ -15,7 +15,7 @@ public interface AnswerRepository extends JpaRepository { SELECT a FROM Answer a JOIN Review r ON a.reviewId = r.id WHERE r.reviewGroupId = :reviewGroupId AND a.questionId IN :questionIds - ORDER BY r.createdAt DESC + ORDER BY r.createdAt DESC, r.id DESC LIMIT :limit """) List findReceivedAnswersByQuestionIds(long reviewGroupId, Collection questionIds, int limit); diff --git a/backend/src/main/java/reviewme/review/repository/ReviewRepository.java b/backend/src/main/java/reviewme/review/repository/ReviewRepository.java index 3a0600ad9..90119fa0b 100644 --- a/backend/src/main/java/reviewme/review/repository/ReviewRepository.java +++ b/backend/src/main/java/reviewme/review/repository/ReviewRepository.java @@ -12,7 +12,7 @@ public interface ReviewRepository extends JpaRepository { @Query(""" SELECT r FROM Review r WHERE r.reviewGroupId = :reviewGroupId - ORDER BY r.createdAt DESC + ORDER BY r.createdAt DESC, r.id DESC """) List findAllByGroupId(long reviewGroupId); From fa7d3d304f6963fee8af30587f0dc0b4b5740ad8 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Thu, 21 Nov 2024 00:52:01 +0900 Subject: [PATCH 2/8] =?UTF-8?q?[BE]=20feat:=20Spring=20Cache=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9=20(#792)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 1 + .../reviewme/config/CacheManagerConfig.java | 19 +++++++++++++++++++ .../service/mapper/TemplateMapper.java | 8 +++++--- backend/src/test/resources/application.yml | 2 ++ 4 files changed, 27 insertions(+), 3 deletions(-) create mode 100644 backend/src/main/java/reviewme/config/CacheManagerConfig.java diff --git a/backend/build.gradle b/backend/build.gradle index cea87d9cf..99bb632fb 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -29,6 +29,7 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.springframework.boot:spring-boot-starter-cache' implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:2.5.0' implementation 'org.springframework.boot:spring-boot-starter-actuator' implementation 'io.micrometer:micrometer-registry-prometheus' diff --git a/backend/src/main/java/reviewme/config/CacheManagerConfig.java b/backend/src/main/java/reviewme/config/CacheManagerConfig.java new file mode 100644 index 000000000..21151523f --- /dev/null +++ b/backend/src/main/java/reviewme/config/CacheManagerConfig.java @@ -0,0 +1,19 @@ +package reviewme.config; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Profile; + +@Configuration +@EnableCaching +public class CacheManagerConfig { + + @Profile({"local", "dev", "prod"}) + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager(); + } +} diff --git a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java index 7151003d5..5225bd7e6 100644 --- a/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java +++ b/backend/src/main/java/reviewme/template/service/mapper/TemplateMapper.java @@ -2,6 +2,7 @@ import java.util.List; import lombok.RequiredArgsConstructor; +import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; import reviewme.template.domain.OptionGroup; import reviewme.template.domain.OptionItem; @@ -14,9 +15,6 @@ import reviewme.template.domain.SectionQuestion; import reviewme.template.domain.Template; import reviewme.template.domain.TemplateSection; -import reviewme.template.service.exception.MissingOptionItemsInOptionGroupException; -import reviewme.template.service.exception.SectionInTemplateNotFoundException; -import reviewme.template.service.exception.TemplateNotFoundByReviewGroupException; import reviewme.template.repository.SectionRepository; import reviewme.template.repository.TemplateRepository; import reviewme.template.service.dto.response.OptionGroupResponse; @@ -24,7 +22,10 @@ import reviewme.template.service.dto.response.QuestionResponse; import reviewme.template.service.dto.response.SectionResponse; import reviewme.template.service.dto.response.TemplateResponse; +import reviewme.template.service.exception.MissingOptionItemsInOptionGroupException; import reviewme.template.service.exception.QuestionInSectionNotFoundException; +import reviewme.template.service.exception.SectionInTemplateNotFoundException; +import reviewme.template.service.exception.TemplateNotFoundByReviewGroupException; @Component @RequiredArgsConstructor @@ -38,6 +39,7 @@ public class TemplateMapper { private final OptionGroupRepository optionGroupRepository; private final OptionItemRepository optionItemRepository; + @Cacheable(value = "template_response", key = "#reviewGroup.templateId") public TemplateResponse mapToTemplateResponse(ReviewGroup reviewGroup) { Template template = templateRepository.findById(reviewGroup.getTemplateId()) .orElseThrow(() -> new TemplateNotFoundByReviewGroupException( diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index f18542246..9b8436a86 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -13,6 +13,8 @@ spring: ddl-auto: update flyway: enabled: false + cache: + type: none springdoc: swagger-ui: From 0522c5fc2c7322787e3feaa259385a02be8c17cf Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Sat, 23 Nov 2024 00:32:24 +0900 Subject: [PATCH 3/8] =?UTF-8?q?[BE]=20refactor:=20question=20=ED=83=80?= =?UTF-8?q?=EC=9E=85=EA=B3=BC=20=EB=8B=B5=EB=B3=80=20=EB=82=B4=EC=9A=A9=20?= =?UTF-8?q?=EC=9D=BC=EC=B9=98=20=EA=B2=80=EC=A6=9D=20=EB=B0=8F=20=EB=A7=A4?= =?UTF-8?q?=ED=95=91=20=EB=A1=9C=EC=A7=81=20=EA=B0=9C=EC=84=A0=20(#958)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: 타입에 대한 분기를 answerMapper 안에서 처리할 수 있도록 * refactor: 사용하지 않는 코드 제거 * refactor: 하나의 테스트에 한가지를 테스트하도록 * refactor: 추상화를 더욱 활용하도록 * style: 개행 통일 * refactor: getQuestionType 없애고 바로 supports를 사용하도록 * refactor: 다른 유형의 답변이 동시에 입력되는지에 대한 검증 삭제 * refactor: 함수 이름 변경 * style: 띄어쓰기 추가 * refactor: 접근제한자 변경 * refactor: 접근제한자 변경 - 팀의 컨벤션을 위해 & 유지보수 편한 코드를 위해 접근 제한자를 public & private 으로 통일 * refactor: AnswerMapper 변경 - abstract class -> interface * fix: 컴파일 에러 해결 - abstract class -> interface 변환 하면서 extends -> implements 변경되지 않은 부분 변경 --- .../dto/request/ReviewAnswerRequest.java | 9 +- .../review/service/mapper/AnswerMapper.java | 2 +- .../service/mapper/CheckboxAnswerMapper.java | 7 +- .../review/service/mapper/ReviewMapper.java | 10 --- .../service/mapper/TextAnswerMapper.java | 8 +- .../mapper/AnswerMapperFactoryTest.java | 10 +-- .../mapper/CheckboxAnswerMapperTest.java | 17 ++-- .../service/mapper/ReviewMapperTest.java | 83 ++++++++----------- .../service/mapper/TextAnswerMapperTest.java | 25 +++--- 9 files changed, 71 insertions(+), 100 deletions(-) diff --git a/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java b/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java index 60233be2d..85d89cd7e 100644 --- a/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java +++ b/backend/src/main/java/reviewme/review/service/dto/request/ReviewAnswerRequest.java @@ -15,11 +15,12 @@ public record ReviewAnswerRequest( @Nullable String text ) { - public boolean hasTextAnswer() { - return text != null && !text.isEmpty(); + + public boolean hasNoText() { + return text == null || text.isBlank(); } - public boolean hasCheckboxAnswer() { - return selectedOptionIds != null && !selectedOptionIds.isEmpty(); + public boolean hasNoSelectedOptions() { + return selectedOptionIds == null || selectedOptionIds.isEmpty(); } } diff --git a/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java b/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java index 1181808a5..87ee4c511 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/AnswerMapper.java @@ -1,8 +1,8 @@ package reviewme.review.service.mapper; -import reviewme.template.domain.QuestionType; import reviewme.review.domain.Answer; import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.template.domain.QuestionType; public interface AnswerMapper { diff --git a/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java b/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java index 7fb87b0dc..2829890cd 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/CheckboxAnswerMapper.java @@ -1,10 +1,9 @@ package reviewme.review.service.mapper; import org.springframework.stereotype.Component; -import reviewme.template.domain.QuestionType; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.service.dto.request.ReviewAnswerRequest; -import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; +import reviewme.template.domain.QuestionType; @Component public class CheckboxAnswerMapper implements AnswerMapper { @@ -16,8 +15,8 @@ public boolean supports(QuestionType questionType) { @Override public CheckboxAnswer mapToAnswer(ReviewAnswerRequest answerRequest) { - if (answerRequest.text() != null) { - throw new CheckBoxAnswerIncludedTextException(answerRequest.questionId()); + if (answerRequest.hasNoSelectedOptions()) { + return null; } return new CheckboxAnswer(answerRequest.questionId(), answerRequest.selectedOptionIds()); } diff --git a/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java b/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java index 58d0c6a6f..499a5ea19 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/ReviewMapper.java @@ -62,20 +62,10 @@ private List getAnswersByQuestionType(ReviewRegisterRequest request) { private Answer mapRequestToAnswer(Map questions, ReviewAnswerRequest answerRequest) { Question question = questions.get(answerRequest.questionId()); - if (question == null) { throw new SubmittedQuestionNotFoundException(answerRequest.questionId()); } - // TODO: 아래 코드를 삭제해야 한다 - if (question.isSelectable() && answerRequest.selectedOptionIds() != null && answerRequest.selectedOptionIds().isEmpty()) { - return null; - } - if (!question.isSelectable() && answerRequest.text() != null && answerRequest.text().isEmpty()) { - return null; - } - // END - AnswerMapper answerMapper = answerMapperFactory.getAnswerMapper(question.getQuestionType()); return answerMapper.mapToAnswer(answerRequest); } diff --git a/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java b/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java index 48bd55789..6f28faedd 100644 --- a/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java +++ b/backend/src/main/java/reviewme/review/service/mapper/TextAnswerMapper.java @@ -1,10 +1,9 @@ package reviewme.review.service.mapper; import org.springframework.stereotype.Component; -import reviewme.template.domain.QuestionType; import reviewme.review.domain.TextAnswer; import reviewme.review.service.dto.request.ReviewAnswerRequest; -import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; +import reviewme.template.domain.QuestionType; @Component public class TextAnswerMapper implements AnswerMapper { @@ -16,12 +15,9 @@ public boolean supports(QuestionType questionType) { @Override public TextAnswer mapToAnswer(ReviewAnswerRequest answerRequest) { - if (!answerRequest.hasTextAnswer()) { + if (answerRequest.hasNoText()) { return null; } - if (answerRequest.selectedOptionIds() != null) { - throw new TextAnswerIncludedOptionItemException(answerRequest.questionId()); - } return new TextAnswer(answerRequest.questionId(), answerRequest.text()); } } diff --git a/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java b/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java index bdf37e905..f18dc74f3 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/AnswerMapperFactoryTest.java @@ -8,9 +8,9 @@ import org.junit.jupiter.api.extension.ExtendWith; import org.springframework.boot.test.system.CapturedOutput; import org.springframework.boot.test.system.OutputCaptureExtension; -import reviewme.template.domain.QuestionType; import reviewme.review.domain.Answer; import reviewme.review.service.dto.request.ReviewAnswerRequest; +import reviewme.template.domain.QuestionType; @ExtendWith(OutputCaptureExtension.class) class AnswerMapperFactoryTest { @@ -18,13 +18,13 @@ class AnswerMapperFactoryTest { private final AnswerMapper answerMapper = new AnswerMapper() { @Override - public boolean supports(QuestionType questionType) { - return questionType == QuestionType.CHECKBOX; + public Answer mapToAnswer(ReviewAnswerRequest answerRequest) { + return null; } @Override - public Answer mapToAnswer(ReviewAnswerRequest answerRequest) { - return null; + public boolean supports(QuestionType questionType) { + return questionType == QuestionType.CHECKBOX; } }; diff --git a/backend/src/test/java/reviewme/review/service/mapper/CheckboxAnswerMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/CheckboxAnswerMapperTest.java index eb2d96f98..c05b4553f 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/CheckboxAnswerMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/CheckboxAnswerMapperTest.java @@ -1,14 +1,14 @@ package reviewme.review.service.mapper; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullAndEmptySource; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.CheckboxAnswerSelectedOption; import reviewme.review.service.dto.request.ReviewAnswerRequest; -import reviewme.review.service.exception.CheckBoxAnswerIncludedTextException; class CheckboxAnswerMapperTest { @@ -28,16 +28,17 @@ class CheckboxAnswerMapperTest { .containsExactly(1L, 2L, 3L); } - @Test - void 체크박스_답변_요청에_텍스트가_포함되어_있으면_예외를_발생시킨다() { + @ParameterizedTest + @NullAndEmptySource + void 체크박스_답변이_비어있는_경우_null로_매핑한다(List selectedOptionIds) { // given - ReviewAnswerRequest request = new ReviewAnswerRequest(1L, List.of(1L, 2L, 3L), "text"); + ReviewAnswerRequest request = new ReviewAnswerRequest(1L, selectedOptionIds, null); + CheckboxAnswerMapper mapper = new CheckboxAnswerMapper(); // when - CheckboxAnswerMapper mapper = new CheckboxAnswerMapper(); + CheckboxAnswer actual = mapper.mapToAnswer(request); // then - assertThatThrownBy(() -> mapper.mapToAnswer(request)) - .isInstanceOf(CheckBoxAnswerIncludedTextException.class); + assertThat(actual).isNull(); } } diff --git a/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java index 4065c63de..2a624ccf5 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/ReviewMapperTest.java @@ -2,7 +2,6 @@ import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.junit.jupiter.api.Assertions.assertAll; import static reviewme.fixture.OptionGroupFixture.선택지_그룹; import static reviewme.fixture.OptionItemFixture.선택지; import static reviewme.fixture.QuestionFixture.서술형_옵션_질문; @@ -16,12 +15,6 @@ import java.util.List; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; -import reviewme.template.domain.OptionGroup; -import reviewme.template.domain.OptionItem; -import reviewme.template.domain.Question; -import reviewme.template.repository.OptionGroupRepository; -import reviewme.template.repository.OptionItemRepository; -import reviewme.template.repository.QuestionRepository; import reviewme.review.domain.CheckboxAnswer; import reviewme.review.domain.Review; import reviewme.review.domain.TextAnswer; @@ -31,7 +24,13 @@ import reviewme.reviewgroup.domain.ReviewGroup; import reviewme.reviewgroup.repository.ReviewGroupRepository; import reviewme.support.ServiceTest; +import reviewme.template.domain.OptionGroup; +import reviewme.template.domain.OptionItem; +import reviewme.template.domain.Question; import reviewme.template.domain.Section; +import reviewme.template.repository.OptionGroupRepository; +import reviewme.template.repository.OptionItemRepository; +import reviewme.template.repository.QuestionRepository; import reviewme.template.repository.SectionRepository; import reviewme.template.repository.TemplateRepository; @@ -60,7 +59,7 @@ class ReviewMapperTest { private TemplateRepository templateRepository; @Test - void 텍스트가_포함된_리뷰를_생성한다() { + void 서술형_답변을_매핑한다() { // given ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); @@ -81,7 +80,7 @@ class ReviewMapperTest { } @Test - void 체크박스가_포함된_리뷰를_생성한다() { + void 선택형_답변을_매핑한다() { // given ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); @@ -106,57 +105,47 @@ class ReviewMapperTest { } @Test - void 필수가_아닌_질문에_답변이_없을_경우_답변을_생성하지_않는다() { + void 필수가_아닌_서술형_질문에_답변이_없으면_매핑하지_않는다() { // given ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + Question question = questionRepository.save(서술형_옵션_질문()); + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); + templateRepository.save(템플릿(List.of(section.getId()))); - Question requiredTextQuestion = questionRepository.save(서술형_필수_질문()); - Question optionalTextQuestion = questionRepository.save(서술형_옵션_질문()); + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(question.getId(), null, ""); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest( + reviewGroup.getReviewRequestCode(), List.of(answerRequest)); - Question requeiredCheckBoxQuestion = questionRepository.save(선택형_필수_질문()); - OptionGroup optionGroup1 = optionGroupRepository.save(선택지_그룹(requeiredCheckBoxQuestion.getId())); - OptionItem optionItem1 = optionItemRepository.save(선택지(optionGroup1.getId())); - OptionItem optionItem2 = optionItemRepository.save(선택지(optionGroup1.getId())); + // when + Review review = reviewMapper.mapToReview(reviewRegisterRequest); - Question optionalCheckBoxQuestion = questionRepository.save(선택형_옵션_질문()); - OptionGroup optionGroup2 = optionGroupRepository.save(선택지_그룹(optionalCheckBoxQuestion.getId())); - OptionItem optionItem3 = optionItemRepository.save(선택지(optionGroup2.getId())); - OptionItem optionItem4 = optionItemRepository.save(선택지(optionGroup2.getId())); + // then + assertThat(review.getAnswersByType(TextAnswer.class)) + .extracting(TextAnswer::getQuestionId) + .isEmpty(); + } - Section section = sectionRepository.save(항상_보이는_섹션( - List.of(requiredTextQuestion.getId(), optionalTextQuestion.getId(), - requeiredCheckBoxQuestion.getId(), optionalCheckBoxQuestion.getId()))); + @Test + void 필수가_아닌_선택형_질문에_답변이_없으면_매핑하지_않는다() { + // given + ReviewGroup reviewGroup = reviewGroupRepository.save(리뷰_그룹()); + Question question = questionRepository.save(선택형_옵션_질문()); + OptionGroup optionGroup = optionGroupRepository.save(선택지_그룹(question.getId())); + + Section section = sectionRepository.save(항상_보이는_섹션(List.of(question.getId()))); templateRepository.save(템플릿(List.of(section.getId()))); - String textAnswer = "답".repeat(20); - ReviewAnswerRequest requiredTextAnswerRequest = new ReviewAnswerRequest( - requiredTextQuestion.getId(), null, textAnswer - ); - ReviewAnswerRequest optionalTextAnswerRequest = new ReviewAnswerRequest( - optionalTextQuestion.getId(), null, "" - ); - ReviewAnswerRequest requiredCheckBoxAnswerRequest = new ReviewAnswerRequest( - requeiredCheckBoxQuestion.getId(), List.of(optionItem1.getId()), null - ); - ReviewAnswerRequest optionalCheckBoxAnswerRequest = new ReviewAnswerRequest( - optionalCheckBoxQuestion.getId(), List.of(), null - ); - ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest(reviewGroup.getReviewRequestCode(), - List.of(requiredTextAnswerRequest, optionalTextAnswerRequest, - requiredCheckBoxAnswerRequest, optionalCheckBoxAnswerRequest)); + ReviewAnswerRequest answerRequest = new ReviewAnswerRequest(question.getId(), List.of(), null); + ReviewRegisterRequest reviewRegisterRequest = new ReviewRegisterRequest( + reviewGroup.getReviewRequestCode(), List.of(answerRequest)); // when Review review = reviewMapper.mapToReview(reviewRegisterRequest); // then - assertAll( - () -> assertThat(review.getAnswersByType(TextAnswer.class)) - .extracting(TextAnswer::getQuestionId) - .containsExactly(requiredTextQuestion.getId()), - () -> assertThat(review.getAnswersByType(CheckboxAnswer.class)) - .extracting(CheckboxAnswer::getQuestionId) - .containsExactly(requeiredCheckBoxQuestion.getId()) - ); + assertThat(review.getAnswersByType(CheckboxAnswer.class)) + .extracting(CheckboxAnswer::getQuestionId) + .isEmpty(); } @Test diff --git a/backend/src/test/java/reviewme/review/service/mapper/TextAnswerMapperTest.java b/backend/src/test/java/reviewme/review/service/mapper/TextAnswerMapperTest.java index 841e2d5a3..b7fc960cf 100644 --- a/backend/src/test/java/reviewme/review/service/mapper/TextAnswerMapperTest.java +++ b/backend/src/test/java/reviewme/review/service/mapper/TextAnswerMapperTest.java @@ -1,23 +1,16 @@ package reviewme.review.service.mapper; import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import java.util.List; import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.NullSource; +import org.junit.jupiter.params.provider.ValueSource; import reviewme.review.domain.TextAnswer; import reviewme.review.service.dto.request.ReviewAnswerRequest; -import reviewme.review.service.exception.TextAnswerIncludedOptionItemException; class TextAnswerMapperTest { - /* - TODO: Request를 추상화해야 할까요? - 떠오르는 방법은 아래와 같습니다. - 1: static factory method를 사용 -> 걷잡을 수 없어지지 않을까요? - 2: 다른 방식으로 추상화 ? - */ - @Test void 텍스트_답변을_요청으로부터_매핑한다() { // given @@ -31,16 +24,18 @@ class TextAnswerMapperTest { assertThat(actual.getContent()).isEqualTo("text"); } - @Test - void 텍스트_답변_요청에_옵션이_포함되어_있으면_예외를_발생시킨다() { + @ParameterizedTest + @NullSource + @ValueSource(strings = {"", " "}) + void 텍스트_답변이_비어있는_경우_null로_매핑한다(String text) { // given - ReviewAnswerRequest request = new ReviewAnswerRequest(1L, List.of(1L), "text"); + ReviewAnswerRequest request = new ReviewAnswerRequest(1L, null, text); // when TextAnswerMapper mapper = new TextAnswerMapper(); + TextAnswer actual = mapper.mapToAnswer(request); // then - assertThatThrownBy(() -> mapper.mapToAnswer(request)) - .isInstanceOf(TextAnswerIncludedOptionItemException.class); + assertThat(actual).isNull(); } } From 1c99dd839f3c08eb2606cab87c7d1e614867ca35 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Thu, 28 Nov 2024 22:30:55 +0900 Subject: [PATCH 4/8] =?UTF-8?q?[BE]=20refactor:=20=EC=9A=94=EC=B2=AD=20?= =?UTF-8?q?=EC=A0=9C=ED=95=9C=20=EA=B4=80=EB=A0=A8=20=EC=BD=94=EB=93=9C=20?= =?UTF-8?q?=EC=A0=9C=EA=B1=B0=20(#983)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * refactor: request limit 코드 제거 * refactor: 로컬, 테스트의 request limit 변수 제거 --- .../requestlimit/RequestLimitInterceptor.java | 48 ------------ .../requestlimit/RequestLimitProperties.java | 8 -- .../requestlimit/RequestLimitRedisConfig.java | 34 --------- .../requestlimit/RequestLimitWebConfig.java | 20 ----- .../requestlimit/TooManyRequestException.java | 13 ---- .../global/GlobalExceptionHandler.java | 6 -- backend/src/main/resources/application.yml | 6 -- .../RequestLimitInterceptorTest.java | 75 ------------------- backend/src/test/resources/application.yml | 6 -- 9 files changed, 216 deletions(-) delete mode 100644 backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java delete mode 100644 backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java delete mode 100644 backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java delete mode 100644 backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java delete mode 100644 backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java delete mode 100644 backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java diff --git a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java deleted file mode 100644 index ef25b711e..000000000 --- a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitInterceptor.java +++ /dev/null @@ -1,48 +0,0 @@ -package reviewme.config.requestlimit; - -import static org.springframework.http.HttpHeaders.USER_AGENT; - -import jakarta.servlet.http.HttpServletRequest; -import jakarta.servlet.http.HttpServletResponse; -import lombok.RequiredArgsConstructor; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; -import org.springframework.http.HttpMethod; -import org.springframework.stereotype.Component; -import org.springframework.web.servlet.HandlerInterceptor; - -@Component -@EnableConfigurationProperties(RequestLimitProperties.class) -@RequiredArgsConstructor -public class RequestLimitInterceptor implements HandlerInterceptor { - - private final RedisTemplate redisTemplate; - private final RequestLimitProperties requestLimitProperties; - - @Override - public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { - if (!HttpMethod.POST.matches(request.getMethod())) { - return true; - } - - String key = generateRequestKey(request); - ValueOperations valueOperations = redisTemplate.opsForValue(); - valueOperations.setIfAbsent(key, 0L, requestLimitProperties.duration()); - redisTemplate.expire(key, requestLimitProperties.duration()); - - long requestCount = valueOperations.increment(key); - if (requestCount > requestLimitProperties.threshold()) { - throw new TooManyRequestException(key); - } - return true; - } - - private String generateRequestKey(HttpServletRequest request) { - String requestURI = request.getRequestURI(); - String remoteAddr = request.getRemoteAddr(); - String userAgent = request.getHeader(USER_AGENT); - - return String.format("RequestURI: %s, RemoteAddr: %s, UserAgent: %s", requestURI, remoteAddr, userAgent); - } -} diff --git a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java deleted file mode 100644 index 558378094..000000000 --- a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitProperties.java +++ /dev/null @@ -1,8 +0,0 @@ -package reviewme.config.requestlimit; - -import java.time.Duration; -import org.springframework.boot.context.properties.ConfigurationProperties; - -@ConfigurationProperties(prefix = "request-limit") -public record RequestLimitProperties(long threshold, Duration duration, String host, int port) { -} diff --git a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java deleted file mode 100644 index d8bb458a9..000000000 --- a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitRedisConfig.java +++ /dev/null @@ -1,34 +0,0 @@ -package reviewme.config.requestlimit; - -import lombok.RequiredArgsConstructor; -import org.springframework.boot.context.properties.EnableConfigurationProperties; -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.GenericToStringSerializer; - -@Configuration -@EnableConfigurationProperties(RequestLimitProperties.class) -@RequiredArgsConstructor -public class RequestLimitRedisConfig { - - private final RequestLimitProperties requestLimitProperties; - - @Bean - public RedisConnectionFactory redisConnectionFactory() { - return new LettuceConnectionFactory( - requestLimitProperties.host(), requestLimitProperties.port() - ); - } - - @Bean - public RedisTemplate requestLimitRedisTemplate() { - RedisTemplate redisTemplate = new RedisTemplate<>(); - redisTemplate.setConnectionFactory(redisConnectionFactory()); - redisTemplate.setValueSerializer(new GenericToStringSerializer<>(Long.class)); - - return redisTemplate; - } -} diff --git a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java b/backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java deleted file mode 100644 index 19f3b2fe4..000000000 --- a/backend/src/main/java/reviewme/config/requestlimit/RequestLimitWebConfig.java +++ /dev/null @@ -1,20 +0,0 @@ -package reviewme.config.requestlimit; - -import lombok.RequiredArgsConstructor; -import org.springframework.context.annotation.Configuration; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.web.servlet.config.annotation.InterceptorRegistry; -import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; - -@Configuration -@RequiredArgsConstructor -public class RequestLimitWebConfig implements WebMvcConfigurer { - - private final RedisTemplate redisTemplate; - private final RequestLimitProperties requestLimitProperties; - - @Override - public void addInterceptors(InterceptorRegistry registry) { - registry.addInterceptor(new RequestLimitInterceptor(redisTemplate, requestLimitProperties)); - } -} diff --git a/backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java b/backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java deleted file mode 100644 index 544fb5885..000000000 --- a/backend/src/main/java/reviewme/config/requestlimit/TooManyRequestException.java +++ /dev/null @@ -1,13 +0,0 @@ -package reviewme.config.requestlimit; - -import lombok.extern.slf4j.Slf4j; -import reviewme.global.exception.ReviewMeException; - -@Slf4j -public class TooManyRequestException extends ReviewMeException { - - public TooManyRequestException(String requestKey) { - super("짧은 시간 안에 너무 많은 동일한 요청이 일어났어요. 잠시 후 다시 시도해주세요."); - log.warn("Too many request received - request: {}", requestKey); - } -} diff --git a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java index 161e43172..7724dd90e 100644 --- a/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java +++ b/backend/src/main/java/reviewme/global/GlobalExceptionHandler.java @@ -22,7 +22,6 @@ import org.springframework.web.servlet.resource.NoResourceFoundException; import reviewme.global.exception.BadRequestException; import reviewme.global.exception.DataInconsistencyException; -import reviewme.config.requestlimit.TooManyRequestException; import reviewme.global.exception.FieldErrorResponse; import reviewme.global.exception.NotFoundException; import reviewme.global.exception.UnauthorizedException; @@ -51,11 +50,6 @@ public ProblemDetail handleDataConsistencyException(DataInconsistencyException e return ProblemDetail.forStatusAndDetail(HttpStatus.INTERNAL_SERVER_ERROR, ex.getErrorMessage()); } - @ExceptionHandler(TooManyRequestException.class) - public ProblemDetail handleDuplicateRequestException(TooManyRequestException ex) { - return ProblemDetail.forStatusAndDetail(HttpStatus.TOO_MANY_REQUESTS, ex.getErrorMessage()); - } - @ExceptionHandler(Exception.class) public ProblemDetail handleException(Exception ex) { log.error("Internal server error has occurred", ex); diff --git a/backend/src/main/resources/application.yml b/backend/src/main/resources/application.yml index aa0160b1f..45df6e2cb 100644 --- a/backend/src/main/resources/application.yml +++ b/backend/src/main/resources/application.yml @@ -37,9 +37,3 @@ cors: allowed-origins: - http://localhost - https://localhost - -request-limit: - threshold: 3 - duration: 1s - host: localhost - port: 6379 diff --git a/backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java b/backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java deleted file mode 100644 index 969040683..000000000 --- a/backend/src/test/java/reviewme/config/requestlimit/RequestLimitInterceptorTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package reviewme.config.requestlimit; - -import static org.assertj.core.api.Assertions.assertThatThrownBy; -import static org.assertj.core.api.AssertionsForClassTypes.assertThat; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.verify; -import static org.springframework.http.HttpHeaders.USER_AGENT; - -import jakarta.servlet.http.HttpServletRequest; -import java.time.Duration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; - -class RequestLimitInterceptorTest { - - private final HttpServletRequest request = mock(HttpServletRequest.class); - private final RedisTemplate redisTemplate = mock(RedisTemplate.class); - private final ValueOperations valueOperations = mock(ValueOperations.class); - private final RequestLimitProperties requestLimitProperties = mock(RequestLimitProperties.class); - private final RequestLimitInterceptor interceptor = new RequestLimitInterceptor(redisTemplate, requestLimitProperties); - private final String requestKey = "RequestURI: /api/v2/reviews, RemoteAddr: localhost, UserAgent: Postman"; - - @BeforeEach - void setUp() { - given(request.getMethod()).willReturn("POST"); - given(request.getRequestURI()).willReturn("/api/v2/reviews"); - given(request.getRemoteAddr()).willReturn("localhost"); - given(request.getHeader(USER_AGENT)).willReturn("Postman"); - - given(redisTemplate.opsForValue()).willReturn(valueOperations); - given(requestLimitProperties.duration()).willReturn(Duration.ofSeconds(1)); - given(requestLimitProperties.threshold()).willReturn(3L); - } - - @Test - void POST_요청이_아니면_통과한다() { - // given - given(request.getMethod()).willReturn("GET"); - - // when - boolean result = interceptor.preHandle(request, null, null); - - // then - assertThat(result).isTrue(); - } - - @Test - void 특정_POST_요청이_처음이_아니며_최대_빈도보다_작을_경우_빈도를_1증가시킨다() { - // given - long requestCount = 1; - given(valueOperations.get(anyString())).willReturn(requestCount); - - // when - boolean result = interceptor.preHandle(request, null, null); - - // then - assertThat(result).isTrue(); - verify(valueOperations).increment(requestKey); - } - - @Test - void 특정_POST_요청이_처음이_아니며_최대_빈도보다_클_경우_예외를_발생시킨다() { - // given - long maxRequestCount = 3; - given(valueOperations.increment(anyString())).willReturn(maxRequestCount + 1); - - // when & then - assertThatThrownBy(() -> interceptor.preHandle(request, null, null)) - .isInstanceOf(TooManyRequestException.class); - } -} diff --git a/backend/src/test/resources/application.yml b/backend/src/test/resources/application.yml index 9b8436a86..ccbe2e2ff 100644 --- a/backend/src/test/resources/application.yml +++ b/backend/src/test/resources/application.yml @@ -40,9 +40,3 @@ logging: cors: allowed-origins: - https://allowed-domain.com - -request-limit: - threshold: 3 - duration: 1s - host: localhost - port: 6379 From c07afbb00c3349ed22c8caa3e3ce1aa40f70926c Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Fri, 29 Nov 2024 11:30:57 +0900 Subject: [PATCH 5/8] =?UTF-8?q?[BE]=20fix:=20redis=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=A0=9C=EA=B1=B0=20(#985)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/build.gradle | 1 - .../src/test/java/reviewme/api/ApiTest.java | 19 +------------------ 2 files changed, 1 insertion(+), 19 deletions(-) diff --git a/backend/build.gradle b/backend/build.gradle index 99bb632fb..4981440a7 100644 --- a/backend/build.gradle +++ b/backend/build.gradle @@ -35,7 +35,6 @@ dependencies { implementation 'io.micrometer:micrometer-registry-prometheus' implementation 'org.flywaydb:flyway-core' implementation 'org.flywaydb:flyway-mysql' - implementation 'org.springframework.boot:spring-boot-starter-data-redis' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' diff --git a/backend/src/test/java/reviewme/api/ApiTest.java b/backend/src/test/java/reviewme/api/ApiTest.java index 682a2ea18..20d57db83 100644 --- a/backend/src/test/java/reviewme/api/ApiTest.java +++ b/backend/src/test/java/reviewme/api/ApiTest.java @@ -1,7 +1,5 @@ package reviewme.api; -import static org.mockito.ArgumentMatchers.anyString; -import static org.mockito.BDDMockito.given; import static org.springframework.restdocs.mockmvc.MockMvcRestDocumentation.documentationConfiguration; import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyHeaders; import static org.springframework.restdocs.operation.preprocess.Preprocessors.modifyUris; @@ -17,11 +15,8 @@ import org.apache.http.HttpHeaders; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.extension.ExtendWith; -import org.mockito.Mock; import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest; import org.springframework.boot.test.mock.mockito.MockBean; -import org.springframework.data.redis.core.RedisTemplate; -import org.springframework.data.redis.core.ValueOperations; import org.springframework.http.MediaType; import org.springframework.restdocs.RestDocumentationContextProvider; import org.springframework.restdocs.RestDocumentationExtension; @@ -34,8 +29,8 @@ import reviewme.highlight.controller.HighlightController; import reviewme.highlight.service.HighlightService; import reviewme.review.controller.ReviewController; -import reviewme.review.service.ReviewGatheredLookupService; import reviewme.review.service.ReviewDetailLookupService; +import reviewme.review.service.ReviewGatheredLookupService; import reviewme.review.service.ReviewListLookupService; import reviewme.review.service.ReviewRegisterService; import reviewme.review.service.ReviewSummaryService; @@ -78,12 +73,6 @@ public abstract class ApiTest { @MockBean protected ReviewGroupLookupService reviewGroupLookupService; - @MockBean - protected RedisTemplate redisTemplate; - - @Mock - protected ValueOperations valueOperations; - @MockBean protected ReviewSummaryService reviewSummaryService; @@ -111,12 +100,6 @@ public abstract class ApiTest { } }; - @BeforeEach - void setUpRedisConfig() { - given(redisTemplate.opsForValue()).willReturn(valueOperations); - given(valueOperations.increment(anyString())).willReturn(1L); - } - @BeforeEach void setUpRestDocs(WebApplicationContext context, RestDocumentationContextProvider provider) { UriModifyingOperationPreprocessor uriModifier = modifyUris() From 89080698b9bebef59e79a076527b1a3c6c645611 Mon Sep 17 00:00:00 2001 From: Donghoon Lee Date: Sat, 30 Nov 2024 14:53:34 +0900 Subject: [PATCH 6/8] =?UTF-8?q?cd:=20develop=20=EC=98=A4=EB=9D=BC=ED=81=B4?= =?UTF-8?q?=20=EC=9D=B8=EC=8A=A4=ED=84=B4=EC=8A=A4=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=EC=8B=A4=ED=96=89=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95=20(#984)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/backend-dev-cd.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/backend-dev-cd.yml b/.github/workflows/backend-dev-cd.yml index eec962f53..6a10d7c00 100644 --- a/.github/workflows/backend-dev-cd.yml +++ b/.github/workflows/backend-dev-cd.yml @@ -57,7 +57,7 @@ jobs: deploy: name: Deploy via self-hosted runner needs: build - runs-on: [self-hosted, dev] + runs-on: [self-hosted, dev, oracle] steps: - name: Checkout to secret repository From 6e7a72af670b33f788aa3c9df1ef72499c525762 Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Mon, 2 Dec 2024 14:22:10 +0900 Subject: [PATCH 7/8] =?UTF-8?q?[BE]=20refactor:=20port=20config=20?= =?UTF-8?q?=ED=8C=8C=EC=9D=BC=20=EA=B2=BD=EB=A1=9C=20=EC=9D=B4=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Related to 1b6ee74 --- backend/src/main/resources/ports.yml | 6 ------ 1 file changed, 6 deletions(-) delete mode 100644 backend/src/main/resources/ports.yml diff --git a/backend/src/main/resources/ports.yml b/backend/src/main/resources/ports.yml deleted file mode 100644 index 8b8093829..000000000 --- a/backend/src/main/resources/ports.yml +++ /dev/null @@ -1,6 +0,0 @@ -server: - port: ${SERVER_PORT} - -management: - server: - port: ${ACTUATOR_PORT} From 35d7530cf473f7f6e48b555ca119c02beda6ba1f Mon Sep 17 00:00:00 2001 From: Yeongseo Na Date: Wed, 4 Dec 2024 21:20:28 +0900 Subject: [PATCH 8/8] =?UTF-8?q?chore:=20prod=20cd=EC=97=90=20=EC=82=AC?= =?UTF-8?q?=EC=9A=A9=EB=90=A0=20runner=20=EB=B3=80=EA=B2=BD=20(#992)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 오라클 러너를 사용하도록 변경 --- .github/workflows/backend-prod-cd.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/workflows/backend-prod-cd.yml b/.github/workflows/backend-prod-cd.yml index 41050eb22..f3958ba9e 100644 --- a/.github/workflows/backend-prod-cd.yml +++ b/.github/workflows/backend-prod-cd.yml @@ -52,10 +52,7 @@ jobs: deploy: name: Deploy via self-hosted runner needs: build - strategy: - matrix: - runner: [prod-a, prod-b] - runs-on: [ self-hosted, "${{ matrix.runner }}" ] + runs-on: [self-hosted, prod, oracle] steps: - name: Checkout to secret repository